Práctica de adelgazamiento de la aplicación Alibaba.com: tecnología de tamaño de paquete de iOS y resumen de ROI

1. Introducción

El tamaño del paquete es un indicador importante para medir el rendimiento de la aplicación, afecta directamente la tasa de clics de descarga del usuario (el paquete es demasiado grande y no desea descargarlo), la tasa de éxito de descarga e instalación (la descarga es lento y ya no se necesita), y la tasa de desinstalación de la aplicación (ocupa demasiado espacio y la elimina primero). La lógica de cálculo del tamaño del paquete es muy simple, es la suma del tamaño del disco ocupado por varios tipos de archivos. La tecnología de adelgazamiento de aplicaciones es muy complicada. La complejidad del archivo de código y la estrategia del compilador determinan el tamaño del archivo ejecutable, y la función comercial y la estructura de ingeniería determinan la complejidad del archivo de código. Para el adelgazamiento de la aplicación iOS, las habilidades que deben dominarse incluyen la tecnología de construcción XCode, la tecnología de compilación LLVM, la tecnología de construcción CocoaPods, la tecnología de compresión de imágenes y la tecnología de integración continua. Este artículo resume y refina las técnicas y estrategias de adelgazamiento de la aplicación Alibaba.com, y presenta sistemáticamente el valor comercial, la tecnología de análisis, la tecnología de adelgazamiento y el mecanismo antideterioro del adelgazamiento de la aplicación, para que los lectores puedan comprender sistemáticamente el sistema técnico del adelgazamiento de la aplicación. Este artículo también presenta el ROI de varias técnicas de adelgazamiento basadas en la experiencia práctica, para que los lectores puedan evitar pisar el trueno y desperdiciar recursos en técnicas ineficaces. Espero que te ayude.

2. Valor comercial

2.1 Por cada aumento de 6 MB en el tamaño del paquete, la tasa de conversión de descarga de aplicaciones se reducirá en un 1 %.

En la Conferencia de desarrolladores de Google de 2019, Google proporcionó datos muy detallados: por cada aumento de 6 MB en el tamaño del paquete, la tasa de conversión de descarga de aplicaciones se reducirá en un 1 %. La tasa de conversión varía ligeramente en diferentes regiones. Por cada 10 MB de reducción en el tamaño del paquete APK, la tasa de conversión de descarga promedio global aumentará en un 1,75%. Los países emergentes representan India y Brasil, y la tasa de conversión de descarga aumentará en más de 2,0. % El mercado de gama alta representa a Estados Unidos y Alemania, 1,5%.

Leyenda arriba: APK reducido en 10 MB, tasa de conversión aumentada en diferentes países

Fuente de datos: datos internos de Google Play

El informe de investigación y análisis de datos anterior es anterior a 2019, que se ha rezagado y es solo para referencia

Puede haber 3 razones por las que el tamaño del paquete afecta las tasas de conversión de descarga:

  1. En el entorno de la red celular, los usuarios son reacios a pagar por el tráfico de datos. Cuando el tamaño del paquete supera los 200 MB, la tienda de aplicaciones mostrará un cuadro emergente para recordar al usuario que la descarga puede generar cargos por tráfico.

  2. El tiempo de descarga es demasiado largo, y el usuario no quiere esperar y lo cancela

  3. Problemas de conexión de red durante la descarga

虽然Google Play没有给出不同APP类目的数据,但是从以上三个原因推断,不同类目包大小对下载转化率的影响估计差不多。App Store的用户人群比较高端,可以参考美国和德国的数据。

2.2、20%的人因为存储空间有限而卸载应用程序

clevertap在2021年做了一项调查,他们调查了2000多个移动应用程序用户,询问了他们卸载移动应用程序的主要原因,其中有20%的人因为存储空间有限而卸载应用程序。

最主要的3个原因是:

  1. 他们不再使用该应用程序
  2. 有限的存储空间
  3. 太多的广告。

2.3、App Store 发布和下载限制

兼容iOS8的App,主二进制文件的Text段不能超过60MB, 否则将无法提交App Store。App Store下载包超过200MB,无法使用蜂窝流量下载和更新。

3、分析技术

APP瘦身最终目标是减少App Store的安装包大小和下载包大小,但研发阶段对比XCode构建包大小会更方便,需要理清楚他们之间的口径差异。

3.1、结果指标:App Store安装包大小和下载包大小

查看路径是App Store Connect->TestFlight->交付版本->构建版本元数据->App Store文件大小

3.2、过程指标:XCode构建包大小

XCode构建产物ipa包的大小和App Store的安装大小口径差异很大,通过模拟Apple处理的流程可以得到它们的关系。开发者上传的ipa包里有多个架构的产物,多套尺寸的图片资源,苹果会进行裁剪和二次分发,转化为App Store下载的ipa包。

构建产物ipad包大小:静态库二进制文件(arm64、armv7)、动态库(arm64、armv7)、asset.car(@2x、@3x)、其他资源文件

App Store的安装大小:静态库二进制文件(单架构)、动态库(单架构)、asset.car(单尺寸)、其他资源文件

使用lipo工具拆分单架构

lipo "originalExecutable" -thin arm64 -output "arm64Executable"

使用assetutil工具拆分asset

xcrun --sdk iphoneos assetutil --scale 3 --output "$targetFolder/Assets3.car" "$sourceFolder/Assets.car"

3.3、构建包组成

学习如何治病前,得先了解人体的构成。同样的道理,我们首现要了解iOS工程结构和IPA包结构。

iOS工程结构:iOS工程由壳工程和Pod模块组成,模块有静态库和动态库两种类型。壳工程的构成有主Target、Apple插件Target。模块内部的构成有源代码文件(OC、C、C++的.h和.m)、nib、bunlde、xcassets、多语言文件、各种配置文件(plist、json)。IPA包结构:iOS上传到App Store是IPA包,IPA包解压后是一个文件夹,内部由各种类型的文件构成,主要包括MachO可执行文件、.framework(动态库)、Assets.car、.appex(Apple插件)、.strings(多语言)、.bundle、nib、json、png...。从iOS工程到IPA包: iOS工程构建为IPA包,核心的变化是编译和文件拷贝,静态库里的源代码会被编译为MachO可执行文件,xcassets文件夹会被转化为Assets.car,其他都可以简单理解为文件拷贝。

通过分析构建包的组成,可以判断有哪些优化空间。如果动态库占比太高,就可能有大量可裁剪代码。建议根据资源大小敏感程度划分,比如:MachO executeable(包含所有静态库)、动态库、Apple Extension、assets.car、bundle、nib、音频、视频。

3.4、Pod模块大小

APP瘦身会涉及大量业务模块,离不开业务团队的参与。因此,我们需要分析出每个Pod模块的大小,从而可以横向比较各个业务团队的包大小占比。静态库和动态库计算的原理不同。对于静态库,先解析linkmap数据,计算出Pod模块代码大小,在解析Pods-targetName-resource.sh的资源拷贝代码,计算出拷贝到Pod模块的资源大小。对于动态库,先使用lipo拆分动态库的二进制文件,计算出单架构的代码大小,然后再计算动态库framework内的资源文件,得到动态库的资源文件大小。

3.5、运行时Objc类覆盖率

如果能知道App运行时有哪些类被使用过,就可以下线掉无用的模块或代码文件,Objc类覆盖率指标可以帮到我们。APP运行时,某1个Pod模块被加载的类数量除以所有类数量,可以称为这个模块的Objc类覆盖率。核心技术是判断一个类是否被加载过,下面介绍一个经过线上验证的轻量级方案。ObjC的类第一次被使用时会调用+initialize方法,类被加载过后cls->isInitialized会返回True。isInitialized方法读取了metaClass的data变量里的flags,如果flags里的第29位为1,则返回True。

// objc-class.mm
Class class_initialize(Class cls, id inst) {
    if (!cls->isInitialized()) {
        initializeNonMetaClass (_class_getNonMetaClass(cls, inst));
    }
    return cls;
}


// objc-runtime.h
#define RW_INITIALIZED        (1<<29)
bool isInitialized() {
    return getMeta()->data()->flags & RW_INITIALIZED;
}

3.6、未被使用的图片资源

技术原理是先解析出所有拷贝到构建产物的资源文件,再解析出代码中实际引用到的资源文件,两者的差集就是无用资源。

第一步获取全量资源文件。在Cocoapos工程中,“Pods-targetName-resource.sh”脚本负责拷贝Pod里的文件资源到构建产物,包括所有文件类型bundle、xcassets、json、png。解析该脚本可以得到每个Pod模块都拷贝了哪些图片资源。

// Pods-targetName-resource.sh
install_resource "${PODS_ROOT}/APodName/APodName.framework/APodName.bundle"
install_resource "${PODS_ROOT}/BPodName/BPodName.framework/BPodName.xcassets"
install_resource "${PODS_ROOT}/BPodName/BPodName.framework/xxx.png"

第二步,获取代码中实际引用到的资源文件。OC代码中引用资源文件都是以字符串字面量的形式声明,构建后存放在Mach-O文件"__cstring" section。利用strings解析framework的二进制文件就可以得到代码中所有的字符串声明,然后基于代码的各种引用方式匹配“xxx”,"[email protected]","xxx.png","xxx.bundle/xxx.png"

strings executable | grep 'xxx' > cstrings.txt

3.7、编译时未被引用的类

iOS编译的产物是Mach-o格式的,文件里 __DATA __objc_classrefs 段记录了所有引用过的类的地址,__DATA __objc_classlist段记录了所有类的地址,两者Differ可以得到未被引用类的地址。然后将地址符号化,就可以得到未被引用类信息。因为Objc是动态语言,如果使用runtime动态调用某个class,这种情况扫描不出来。(比如Target Action和 JS Core)

otool -v -s __DATA __objc_classrefs xxxMainClient  #读取__DATA Segment中section为__objc_classrefs的符号
otool -v -s __DATA __objc_classlist xxxMainClient #读取__DATA Segment中section为__objc_classlist的符号
nm -nm xxxMainClient

扫描未使用类的开源工具

4、瘦身技术

包大小瘦身可以从纯技术视角瘦身,也可以从逻辑视角瘦身。

从纯技术视角有两种思路,第一种思路是优化编译逻辑, 第二种思路是删减各种其他类型(非编译产物)的文件。编译优化对业务逻辑无侵入式,风险和成本比较低,但收益通常也不高。删减文件则比较复杂,删减资源文件收益高但成本不小,逐个删减源码文件风险高且收益小。

相比而言,从逻辑的视角瘦身效果会更明显。站在逻辑的视角,工程是由许多功能模块组成的,主要可以分为业务功能模块(首页、搜索、收银台、订单)、基础功能模块(网络库、图片库、中间件、各种三方库...)。大型工程通常几百甚至上千个模块组成,小模块的有几十KB,大的模块有1MB~10+MB。功能模块内聚性强,当某个功能模块可以废弃或被替代时,整体下线的风险和成本比较低,ROI很高。

4.1、瘦身技术大图

根据项目经验,我梳理出各种优化方法的ROI,下面依次介绍。

4.2、组件瘦身

组件瘦身的ROI:类加载率0%的组件 > 无用功能组件 > 重复功能组件

“类加载率0%的组件”是一个完整的Pod模块,模块的代码和资源可以被整体删除,ROI最高。“无用功能组件”通常是模块内某个文件目录,需要处理一些耦合,梳理资源的归属,ROI其次。“重复功能组件”需要适当重构,迁移成本和风险最高,ROI相对较低。

4.2.1、类加载率0%的组件

“类加载率0%的组件”是指运行时没有任何一个类被加载过的组件,它在运行过程中完全没有用到,可以整体下线。统计类加载率时一般会进行采样,有些低频业务组件可能会误报,下线前需要和业务Owner二次确认。

4.2.2、无用功能组件

“无用功能组件”是逻辑上已经没用的组件,它可能代码还有耦合,但逻辑已经没用,通过重构就可以下线。比如AB实验没效果的业务代码、往年大促的业务代码、业务改版后遗留的老业务、已经过时的三方库。

4.2.3、重复功能组件

对于大规模团队,不同业务线可能会引入“重复功能组件”,比如图片选择器、缓存库、UI组件。重复的功能组件会带来不必要的包大小压力,同时也会带来维护成本。

4.3、资源瘦身

资源瘦身ROI:大资源>有损压缩>重复资源>iconfont>多语言文案瘦身>无用资源

4.3.1、大资源

对大资源进行单点优化收益很大,优先分析100KB以上的资源。比如音频文件,我们工程中音频铃声900KB,优化后去掉了700KB。

4.3.2、有损压缩

XCode构建时会做“compile asset catalog”,会重新对图片进行无损压缩。因此使用imageoptim等工具进行无损压缩效果不明显,其中压缩png图片没有效果,压缩jpg图片有一定效果。根据实践经验,icon做有损压缩并不影响视觉体验,压缩率可以达到70%~80%。比如使用tinypng算法压缩一张425.9 KB的png图片,压缩后79.8 KB,压缩率达到81%。业界有不少png压缩工具,我们使用到的有tinypngpngquantpngcrushoptipng(无损)、advpng。实测发现不同的图片,压缩效果最好的工具是不一样,所以压缩图片时可以用每个工具尝试,然后取效果最好的。需要注意的是,压缩APNG图片可能会变成非动图,尽量避免对APNG图片进行压缩,压缩前先识别是否APNG图片。

# 判断是否APNG格式,APNG格式不压缩
function isApng() {
    ret=`grep "acTL" "$1"`
    lastWord="${ret##* }"
    if [[ $lastWord == "matches" ]]; then
        echo "YES"
    else
        echo "NO"
    fi
}

# 使用各种工具压缩png图片
# pngquant
pngquant  256 -s5 --quality 70 --output "${tmpFileName}" -- "${tmpOrigName}" 
# pngcrush
pngcrush -brute -rem alla -nofilecheck -bail -blacken -reduce -cc -- "$tmpOrigName" "$tmpFileName"
# optipng
optipng -o6 -i0 -out "$tmpFileName" -- "$tmpOrigName"
# advpng
cp "$tmpOrigName" "$tmpFileName"
advpng -4 -z -- "$tmpFileName"

# 逐个比较压缩效果,保留效果最好的压缩图片
sizeBefore=`stat -f%z "${tmpOrigName}"`
sizeBefore=`expr $sizeBefore`
sizeNow=`stat -f%z "${tmpFileName}"`
sizeNow=`expr $sizeNow`

if [ "$sizeNow" -lt "$sizeBefore" ]; then
	# 大小变小了
	echo "[advpng] $tmpOrigName"
	retImg="$tmpFileName"
fi

# 记录hash值
img_sum=`/usr/bin/shasum -a 256 "$retImg" | cut -d " " -f1`
cache_file="$cacheFolder/$img_sum.png"

4.3.3、无用资源

业务长期迭代会积累许多无用的资源。通过代码静态扫描,可以分析出没有被引用的图片资源。我们有工程无用资源有12MB,其中有20个模块无用资源超过100KB。

4.3.4、重复图标

不同业务需求会引入重复资源,长期积累也会造成很大浪费。解决方案是在构建时计算资源的哈希值,去重相同哈希资源,并保留源文件名和哈希值的映射表。运行时Hook 资源加载的”imageNamed“方法,根据映射表替换资源名称。

4.3.5、ODR

iOS不能像Android一样,运行时更新Framework。iOS的ODR技术(On Demand Resource)提供了运行时动态下载的能力。如果启动用不到的资源文件,可以通过ODR处理。

4.3.6、iconfont

iconfont支持缩放、修改颜色,它size小,适合用于箭头、占位图等图标场景,使用iconfont可以减少包大小也能提高开发视觉体验的统一性。首先,基于设计规范出一套完整的iconfont,封装iconfont组件,提供易用的API。然后禁止新业务场景增加图片资源,团队形成开发习惯后再逐步替换存量的图片。

4.3.7、多语言文案

对于国际化App,多语言文案也是大头。我们App支持20个语种,每个语种4000多条文案,文案总大小6MB,瘦身后减少了2MB。

4.4、编译优化:

编译优化ROI:Optimization Level > 动态库复用主二进制静态库> 链接器产物压缩

XCode编译优化选项中,Optimization Level效果最明显,建议所有模块构建都开启Oz选项。如果APP有动态库,并且依赖了openssl等基础静态库,建议和APP工程共享,并通过EXPORTED_SYMBOLS_FILE的选型,确保动态库中需要用到的符号都在编译过程保留。如果团队技术储备强,可以自研链接器产物压缩技术,效果也很大。具体效果如下。

4.4.1、精简编译产物Oz:Optimization Level

Optimization Level多个级别,-Oz比-O3的编译产物体积小10%左右。设置-Oz以后,XCode会优化连续的汇编指令,从而减少二进制大小,但副作用是执行速度会变慢。C++工程建议都开启。

主工程Release
Optimization Level :-Oz

Framework工程
Optimization Level :-Oz

4.4.2、LTO(OC/C++)

LTO 是在LLVM链接时,优化跨模块调用代码。根据大家分享的经验,它不同APP包大小优化效果差异较大,需要具体测试。另外它也有一些副作用,建议只在Release包开启。LTO介绍

优点:

  • 将一些函数內联化
  • 去除了一些无用代码
  • 对程序有全局的优化作用

缺点:

  • 降低编译链接速度,只建议在打正式包时开启
  • 降低 link map 可读性(出现XX-lto.thin的类)

主工程设置

主工程Release
Link-Time Optimization 设置为Incremental

framework工程Release
Link-Time Optimization 设置为Incremental

4.4.3、动态库复用主二进制静态库

C++动态库经常用到一些基础库比如openssl、libyuv、libcurl,他们一般是静态库。如果动态库引用了静态库,它编译时默认会内嵌静态库的所有符号。虽然我们可以在动态库中设置只导出需要用到的静态库符号,但是有可能多个动态库都用到了同一个基础库,这样还是会造成基础库的冗余。比如openssl大小1MB,如果A、B两个动态库依赖了openssl,APP也引用了openssl,最终ipa包实际有3个openssl,有2MB大小是冗余的。

这种场景下,最佳解决方案是共享符号表,让动态库可以调用主二进制的基础库符号,从而可以去掉内置的静态库。只要修改XCode的Link配置,无需额外的代码开发。

动态库工程:

1设置当遇到未定义的函数时,动态查找APP主二进制符号表。

2 关闭bitcode

Other Linker Flags -> -undefined dynamic_lookup
Enable Bitcode -> No

3导出动态库需要调用的外部符号,写到一个文件exported_symbols内

nm -u  xxx.framework/xxx > exported_symbols.txt

APP工程:

1配置需要导出exported_symbols文件内的所有符号,避免编译时动态库需要用到的符号被strip掉。

2关闭bitcode。

// exported_symbols.txt是需要被导出的符号文件路径
EXPORTED_SYMBOLS_FILE -> exported_symbols.txt
Enable Bitcode -> No

如果C++动态库里依赖了外部静态库,该静态库的符号默认会被全部导出。实际上动态库只用了其中的部分符号,可以设置导出符号的白名单,从而减少导出没有用到的静态库符号。

注意事项:1APP和framwork工程都要关闭bitcode,否则编译不过。2如果多个动态库都使用同一个基础库,导出的符号表需要取并集。3动态库升级要及时更新符号表,基础库升级要测试兼容性。

4.4.4、链接器产物压缩(黑科技)

iOS工程构建产物是MachO文件,MachO文件中的TEXT段存放了各种只读的数据段,__cstring段存放了普通的C String,__objc_methtype和__objc_methname存放了Objc的方法签名和方法名。比入Objc代码中声明的@"Hello world",底层会产生一个CFString,构建后存放在__cstring中。这些数据很占空间,一般工程至少会有10MB以上,压缩的收益很可观。我们上线后,App Store安装包大小从191MB优化到174MB,减少了16MB。

技术原理:

链接时将TEXT段数据移到__DATA段并压缩,运行时先执行解压代码,解压TEXT段数据存到自定义段中,将代码中对字符串的引用的地址修正为解压后的自定义段。

4.4.5、剥离符号表:Strip Linked Product

Strip Linked Product设置会剥离特定的符号,Debug环境不要设置YES,否则调试时看不到符号。

主工程Release
Deployment Postprocessing :YES
Strip Linked Product :YES
Strip Style :All Symbols(剥离所有符号表和重定向信息)

Framework工程
Deployment Postprocessing :YES
Strip Linked Product :YES
Strip Style :Non-Global Symbols(剥离包括调试信息等非全局的符号,保留外部符号)

说明:

1静态库不能将Strip Style 设置为All Symbols,因为剥离了所有符号的静态库是无法被正常链接的

2去除符号不影响 dSYM 文件中的符号信息,查看崩溃日志时,可以从 dSYM 文件中找对应符号

4.4.6、Symbols Hidden by Default

Symbols Hidden by Default用于设置符号默认可见性,如果设置为YES,XCode会把所有符号都定义为”private extern”,包大小会略有减少。动态库设置为NO,否则会有链接错误。

主工程Release
Symbols Hidden by Default :Yes

Framework工程 静态库/动态库
Symbols Hidden by Default :NO

4.4.7、剔除未引用的C/C++/Swift代码:Dead Code Stripping

Dead Code Stripping开启后会在链接时移除未使用的代码,它对静态语言C/C++/Swift有效,对动态语言OC无效。

主工程
Dead Code Stripping :Yes

4.4.8、Asset Catalog Compiler

Optimization有三个选项,空、time和Space,选择Space可以优化包大小

主工程
Asset Catalog Compiler->Optimization设置为space

4.4.9、C++只导出必要符号:Symbol Visibility

C++的静态库和动态库都只导出必须的符号,默认设置为隐藏所有符号,然后用Visibility Attributes单独控制需要导出的符号

默认隐藏所有C++符号

Other C++ Flags->添加-fvisibility=hidden

设置需要导出的符号

__attribute__((visibility("default"))) void MyFunction1() {} 
__attribute__((visibility("default"))) void MyFunction2() {} 
....

4.5、代码下线

根据Objc类覆盖率统计的结果,可以逐步下线掉未被使用的类。代码文件的量级比较小,下线代码需要仔细确认,避免引起功能问题或crash,ROI比较低。

4.6、Flutter专项

Flutter是单独构建的,引入的内容包括Flutter引擎产物、Flutter业务代码产物。APP如果引入Flutter,需要对Flutter进行专项瘦身。

4.6.1、精简编译产物Oz:Optimization Level

-Oz选项相比Os,收益预估11%,但首屏性能1%~9%的损耗

4.6.2、Dart符号剔除和混淆

根据官方文档,可以通过--dwarf-stack-trace选项去除Dart标准的调试符号。通过--obfuscate 选项,可以将较长的符号替换为短符号,副作用是符号会被混淆。实践效果:Release环境下,--dwarf-stack-trace和--obfuscate选项开启后减少14%的大小

4.6.3、Flutter icon摇树优化

--tree-shake-icons

实践效果:Alibaba.com App开启后减少了300KB左右大小

4.6.4、去除NOTICES文件

实践效果:压缩前700KB,压缩后80KB

5、防劣化机制

5.1、增量标准和集成卡口

包大小瘦身的技术就像各种减肥运动,跑步、跳舞、燃脂瑜伽。但想达到减肥的目标,只靠运动是不够的,还得控制卡路里的摄入,管住嘴的人最后才能减肥成功。因此,我们还需要”新增卡路里的规范”(包大小增量规范)和监工(集成卡口)。持续集成当中加入一个卡口插件,分析构建包的linkemap文件得到模块的大小可,然后对比基线数据,如果违反了包大小增量标准,则禁止集成。

包大小增量规范(参考):

  1. 基线数据:基于特定版本,通过上文提到的”计算模块大小“的方法,计算出每个模块的基线数据
  2. 存量模块:模块增量超过基准数据100KB时禁止集成。设置补偿机制,如果能对存量模块瘦身,可以抵消新增的模块大小。对于特殊情况可以走特殊审批,加入审批流程是启发大家反思。增加那么多是否有价值?有没有带入不必要的资源?
  3. 新模块:新业务功能需要走审批流程,评估新业务价值和包大小增量是否合理。

5.2、横向对比业务健康度

当包大小从技术层面已经优化到极致时,想要进一步瘦身,只能从业务价值的角度去挖掘。如果一个模块磁盘尺寸大,用户使用量却少,那可以认为它对业务的价值较小。因此我们可以定义一个技术指标来衡量模块单位大小的业务贡献度。基于容积率指标,我们就可以横向对比各个业务,要求容积率低的业务做包大小瘦身,或下线不太重要的业务功能。

容积率 = Business PV / Business size
l

6、总结

总结一下包大瘦身的实施路径。第一步,制定目标,跟踪APP下载转化率(App Store Connect Analytics)、APP安装包大小、XCode构建包大小等结果指标。第二步,建设分析体系,包括Pod模块大小分析、Objc类覆盖率分析、无用图片资源分析等。第三步,根据ROI使用各项瘦身技术,组件瘦身>资源瘦身>编译优化>代码下线.第四步,建设防劣化机制,包括增量标准、集成卡口能力、健康度分析。

7、参考

白鲸出海:2019谷歌开发者大会首日看点:Google Play的新变化

www.baijingapp.com/article/248…

googleplaydev:Shrinking APKs, growing installs

medium.com/googleplayd…

clevertap:Why Users Uninstall Apps

clevertap.com/blog/uninst…

Pngcrush

pmt.sourceforge.io/pngcrush/

pngquant

pngquant.org/

OptiPNG

optipng.sourceforge.net/

advpng

www.advancemame.it/doc-advpng.…

Supongo que te gusta

Origin juejin.im/post/7117074938577551368
Recomendado
Clasificación