会员登录 - 用户注册 - 设为首页 - 加入收藏 - 网站地图 Android微信客户端是如何支持R8构建的?!

Android微信客户端是如何支持R8构建的?

时间:2023-06-10 01:20:39 来源:不卡的在线视频网址 作者:时尚 阅读:925次

作者:chrispaul,信客来自微信客户端团队

背景

在之前的户端何支版本,微信Android一直采用Proguard构建Release包,构建3344nn改成什么网址主要原因在于:

Proguard优化足够稳定ApplyMapping也能保证正确性与AutoDex搭配使用,信客生成足够小的户端何支Tinker Patch。

但Proguard也有明显的构建不足之处:

Kotlin版本的升级与Proguard存在不兼容,导致被迫升级Proguard版本;Proguard版本升级导致编译时间变慢,信客超过30min;由于历史原因,户端何支一些keep规则导致包大小无法达到最优;随着AGP的构建升级,将默认采用Google的信客R8来构建以获取更优的Apk性能;

R8的优势

相对于Proguard,R8的户端何支优势在于:

能力支持:R8对Kotlin有更好的支持;构建耗时:虽然我们有增量Proguard编译,但在全量构建时间R8比Proguard更短,构建3344nn改成什么网址开启优化只需要15min左右,信客比Proguard缩短至少一半的户端何支构建时间;开启R8优化,使得将应用程序减少了至少14M的构建包大小优化,这个是我们切换R8的主要原因;

Apk构建流程

AGP 7.2.2 Gradle 7.5

1. 使用Proguard构建

说明:

Proguard生成优化的java字节码,包括提供混淆代码能力;在打Patch apk时,利用Proguard的ApplyMapping能力保证前后构建的代码混淆结果一致;AutoDex确保将前后构建的dalvik字节码分布在相同的dex序列中,为了生成尽可能小的tinker patch;

2. 开启R8后

可见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:Optimize阶段

「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 * 

{

    private

 com.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左右的包大小损失:

「Obfuscated阶段:」

「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. applymapping带来的较多的混淆问题」

「具体分为三类问题:」

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:

(责任编辑:探索)

相关内容
  • 国家动真格了,两会结束仅仅一月,社会已经发生了五大变化
  • 头条搜索网页版上线:搜你想搜的
  • 陈根:告别人类帮助的AutoGPT火了
  • 网站重复(相似)内容致关键词排名下滑,SEO修复方案
  • 微信搜一搜,一天被动吸粉100+的方法
  • 微信搜索正式升级为“微信搜一搜”
  • 福布斯发布2023年AI50榜单:OpenAI、谷歌云、AWS等上榜
  • 搜狗搜索App签到送现金,天天可领取,最高88元~
推荐内容
  • 搜索引擎的检索技术原理详细解析
  • Alpha法律检索工具对新手律师有多重要?—iCourt发布
  • 维纳尔杜姆:很高兴在费耶诺德感受到爱和尊重,但失望没能赢球
  • 微信搜索,终于被搜狗拿下了
  • 消保委发布儿童智能手表试验分析报告,涉及华为、360等12款
  • “促进合理膳食 助力乡村振兴”维爱公益行动走进湖南安乡