作者:chrispaul,信客来自微信客户端团队
在之前的户端何支版本,微信Android一直采用Proguard构建Release包,构建3344nn改成什么网址主要原因在于:
Proguard优化足够稳定ApplyMapping也能保证正确性与AutoDex搭配使用,信客生成足够小的户端何支Tinker Patch。但Proguard也有明显的构建不足之处:
Kotlin版本的升级与Proguard存在不兼容,导致被迫升级Proguard版本;Proguard版本升级导致编译时间变慢,信客超过30min;由于历史原因,户端何支一些keep规则导致包大小无法达到最优;随着AGP的构建升级,将默认采用Google的信客R8来构建以获取更优的Apk性能;相对于Proguard,R8的户端何支优势在于:
能力支持:R8对Kotlin有更好的支持;构建耗时:虽然我们有增量Proguard编译,但在全量构建时间R8比Proguard更短,构建3344nn改成什么网址开启优化只需要15min左右,信客比Proguard缩短至少一半的户端何支构建时间;开启R8优化,使得将应用程序减少了至少14M的构建包大小优化,这个是我们切换R8的主要原因;AGP 7.2.2 Gradle 7.5
❞说明:
Proguard生成优化的java字节码,包括提供混淆代码能力;在打Patch apk时,利用Proguard的ApplyMapping能力保证前后构建的代码混淆结果一致;AutoDex确保将前后构建的dalvik字节码分布在相同的dex序列中,为了生成尽可能小的tinker patch;可见R8省去了dex环节直接将java字节码生成dalvik字节码,由于在Android微信我们大部分发版都是基于Tinker patch的方式进行的,因此接入R8之后必须提供applymapping、autodex的类似能力(如下图),使得打出更小的tinker patch。庆幸的是,R8早已支持applymapping,但并不提供dex重排能力,所以支持applymapping和dexlayout是成功接入R8的重点工作内容。
刚开始在微信版本中开启R8优化和applymapping能力,我们遇到了众多新问题,具体表现为运行时crash,分析原因基本分为两大类问题:
Optimize优化阶段开启applymapping的Obfuscate混淆阶段产生的crash问题下面将重点介绍接入R8遇到的部分疑难杂症并给出具体的解决方案。
「1. Field access被修改」
「分析:」微信一直以来禁用了Field优化,即配置了!field/*规则,但R8并不理解这一行为,导致图中的NAME的access被优化成了public(如下图),导致业务通过getField反射获取字段出现错误的返回,解决的办法可以通过-allowaccessmodification来规避,或者修改子类的access改为public等方式
「2. InvokeDynamic指令导致类合并」
「分析:」业务有的地方会对一些类做一些check,比如检查传入的class是否存在默认构造函数(<init>())
通过crash我们查看字节码发现,kotlin隐式调用接口,会生成 visitInvokeDynamic指令; 给到R8, 会将多个调用的对象进行合并到一个类;而kotlin显式调用接口,会编译生成匿名内部类,给到R8, 不会将多个调用的对象进行合并为一个类;解决此问题我们采用了取巧的方案:为了不让kotlinc生成invoke-dynamic,在kotlinc阶段添加 "-Xsam-conversions=class", 这样就没有 method handler 和callsite class,从而R8就没有机会做类合并;
「3. 强引用的Field变量被shrink」
「分析:」如上图所示,业务刻意通过赋值给强引用变量来防止callback的弱引用被释放导致无法回调,R8同样也不理解这一行为,从而将变量优化掉,但却很难发现此类问题,可以通过添加新的规则来解决:
-keepclassmembers class *{
privatecom.tencent.mm.ui.statusbar.StatusBarHeightWatcher$OnStatusBarHeightChangeCallback mStatusBarHeightCallback;
}
「4. R8行号优化导致Tinker DexDiff变大」
「分析:」发现即使改几句代码,也会导致dexdiff产生接近20M的patch大小。原因是R8在优化的最后环节会对行号进行优化,
// Now code offsets are fixed, compute the mapping file content. if(willComputeProguardMap()) {
// TODO(b/220999985): Refactor line number optimization to be per file and thread it above.DebugRepresentationPredicate representation =
DebugRepresentation.fromFiles(virtualFiles, options);
delayedProguardMapId.set(
runAndWriteMap(
inputApp, appView, namingLens, timing, originalSourceFiles, representation));
}
目的是复用同一个 debug_info_item, 来达到节省包体积的效果,即使代码一句未改,全局的行号优化也会导致bytecode差异较大:
可能的解决方案主要有三种:
删除debugInfo,但势必增加还原crash轨迹的难度,增加开发成本;applymapping阶段复用上次行号优化的结果,改动较大,不利于长期维护;了解到R8在优化行信息时,R8 现在可以使用基于指令偏移量的行表来对共享调试信息对象中的信息进行编码。这可以显著减少行信息的开销。从API级别 26开始的 Android 虚拟机支持在堆栈轨迹中输出指令偏移量(如果方法没有行号信息)。如果使用minsdk 26 或更高版本进行编译,并且没有源文件信息,R8 会完全删除行号信息。为此我们采用了方案3的解决思路,也是顺应了未来低端机型不断被淘汰的大趋势,将R8的行号优化改为基于指令偏移量的行表的方式:
「5. Parameter参数优化」
「分析:」R8会将无用的参数进行优化,applyMapping中会出现混淆结果不一致的现象,比如base mapping中存在:
androidx.appcompat.app.AppCompatDelegate -> androidx.appcompat.app.i:
androidx.collection.ArraySet sActivityDelegates -> a
java.lang.Object sActivityDelegatesLock -> c
1:7:void <clinit>():173:173->
8:15:void <clinit>():175:175->
0:65535:void <init>():271:271->
void addContentView(android.view.View,android.view.ViewGroup$LayoutParams)-> c
android.content.Context attachBaseContext2(android.content.Context)-> d
android.view.View findViewById(int)-> e
int getLocalNightMode()-> f
android.view.MenuInflater getMenuInflater()-> g
void installViewFactory()-> h
void invalidateOptionsMenu()-> i
void onConfigurationChanged(android.content.res.Configuration)-> j
其中,onConfigurationChanged被优化成 void onConfigurationChanged() ,那么applymapping的mapping结果为:
void onConfigurationChanged(android.content.res.Configuration) -> a
而call的调用点还是j方法导致crash,可禁用CallSite优化来规避。
「6. ProtoNormalizer优化导致同一个类出现相同方法」
「分析:」
baseMapping:
androidx.appcompat.view.menu.MenuView$ItemView -> androidx.appcompat.view.menu.j$a:
void initialize(androidx.appcompat.view.menu.MenuItemImpl,int) -> b
applyMapping:
void initialize(int, androidx.appcompat.view.menu.MenuItemImpl) -> c
出现了混淆不一致的现象,可以临时通过禁用该优化来解决, Parameters优化的禁用带来了不到1M的包大小损失。
「7. Out-Of-Line 优化导致无法Tinker Patch」
「分析:」如果多个类如果存在相同实现的方法,那么out-of-line优化目的就是复用同一个方法,由于微信启动时存在一些loader类再dex patch之前做一些必要操作,所以需要对该loader类进行keep,但是out-of-line优化并不受keep限制,因此我们可以临时禁用该优化来解决,带来了不到100K的包大小损失,算法很高级,效果很一般。
「8. EnumUnBoxing 优化导致base和applymapping优化行为不一致」
「分析:」我们发现R8在构建完整包时,优化了enum class, 即EnumUnBoxing优化,生成了一些原始类型的辅助类,原因是原始类型类的内存占用、dexid数、运行时构造开销相比enum class要小一些,这里我们沿用了Proguard的禁用方式来规避,带来了100k左右的包大小损失:
「1. activity类被混淆」
「分析:」在微信中Activity的相关类不应该被混淆,但是在mapping中发现一些activity类被混淆为:
com.tencent.mm.splash.SplashHackActivity -> du2.j:
导致业务想获取activityName失败,原因我们有这样的keep activity规则:
-keep public class * extends android.app.Activity
那么R8只会keep public类型的activity,非public默认混淆,这与proguard有所区别,解决办法较为简单可直接改为:
-keep class * extends android.app.Activity
「具体分为三类问题:」
2.1 类出现重复的相同方法,Failure to verify dex file xxx==/base.apk: Out-of-order method_ids with applyMapping, 特别是horizontal/vertical merge优化最为常见
2.2 接口方法找不到实现方法,java.lang.AbstractMethodError
2.3 内部类access访问受限,java.lang.IllegalAccessError: Illegal class access:
(责任编辑:探索)