查看原文
其他

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

chrispaul 微信客户端技术团队
2024-08-24

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

背景

在之前的版本,微信Android一直采用Proguard构建Release包,主要原因在于:

  1. Proguard优化足够稳定
  2. ApplyMapping也能保证正确性
  3. 与AutoDex搭配使用,生成足够小的Tinker Patch。

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

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

R8的优势

相对于Proguard,R8的优势在于:

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

Apk构建流程

AGP 7.2.2 Gradle 7.5

1. 使用Proguard构建

说明:

  1. Proguard生成优化的java字节码,包括提供混淆代码能力;
  2. 在打Patch apk时,利用Proguard的ApplyMapping能力保证前后构建的代码混淆结果一致;
  3. 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,分析原因基本分为两大类问题:

  1. Optimize优化阶段
  2. 开启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差异较大:

可能的解决方案主要有三种:

  1. 删除debugInfo,但势必增加还原crash轨迹的难度,增加开发成本;
  2. applymapping阶段复用上次行号优化的结果,改动较大,不利于长期维护;
  3. 了解到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 -> <clinit>
    8:15:void <clinit>():175:175 -> <clinit>
    0:65535:void <init>():271:271 -> <init>
    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:******

以上问题也是接入R8最为棘手的问题,并不能通过简单的开关优化选项来规避,必须逐一分析并给出具体的解决方案。这些问题也曾给Google官方提过关于applymapping的issue,回复是并不是他们目前高优先级的工作。可见,applymapping在R8并不能用于生产环境中, 稳定性有待进一步解决;

为此,要解决这些已知问题,必须先了解applymapping的具体流程和算法:

R8基于ApplyMapping的混淆流程图大致如下:


「MappingInterfaces:」

遍历app中所有的interfaces,并从mapping中获取每一个interface的mappedName,如果存在则存至mappedNames中,并同时遍历该interface的所有member进行Naming,并存到memberNames变量里:

if (classNaming != null && (clazz == null || !clazz.isLibraryClass())) { //如果在applyMapping能找到,并且是programClass
      DexString mappedName = factory.createString(classNaming.renamedName); // 获取renamedName
      checkAndAddMappedNames(type, mappedName, classNaming.position);
      classNaming.forAllMemberNaming( // 获取all member 进行Naming
          memberNaming -> addMemberNamings(type, memberNaming, nonPrivateMembers, false));
    }

接下来遍历该interface的所有member,如果该member为method,则先创建临时的parent method: parentReferenceOnCurrentType, 如果parentReferenceOnCurrentType 在 memberNames中 未找到则该member继承了super interface,并补充至memberNames中:

 for (Map<DexReference, MemberNaming> parentMembers : buildUpNames) {
      for (DexReference key : parentMembers.keySet()) { // 遍历所有的member
        if (key.isDexMethod()) { //如果member为method
          DexMethod parentReference = key.asDexMethod();
          DexMethod parentReferenceOnCurrentType =
              factory.createMethod(type, parentReference.proto, parentReference.name); //创建临时method
          if (!memberNames.containsKey(parentReferenceOnCurrentType)) { //如果memberNames不存在,有可能当前的type extend super interface
            addMemberNaming(
                parentReferenceOnCurrentType, parentMembers.get(key), additionalMethodNamings);
          }

最后,获取interface得所有subType的interface类,同样记录所有subtype的classNames和memberNames:

    if (nonPrivateMembers.size() > 0) {
      buildUpNames.addLast(nonPrivateMembers); // 加入deque里面,为了计算subType
      subtypingInfo.forAllImmediateExtendsSubtypes(
          type,
          subType -> computeMapping(subType, buildUpNames, notMappedReferences, subtypingInfo));
      buildUpNames.removeLast();
    } else {
      subtypingInfo.forAllImmediateExtendsSubtypes( //如果该类是接口则获取子类的inteface, 否则获取所有的子类
          type,
          subType -> computeMapping(subType, buildUpNames, notMappedReferences, subtypingInfo));
    }

「MappingClasses:」

具体内容跟MappingInterfaces一致,遍历所有的非interface类,记录至mappedClasses和memberNames中;

    subtypingInfo.forAllImmediateExtendsSubtypes(
        factory.objectType,
        subType -> {
          DexClass dexClass = appView.definitionFor(subType);
          if (dexClass != null && !dexClass.isInterface()) {
            computeMapping(subType, nonPrivateMembers, notMappedReferences, subtypingInfo);
          }
        });

「MappingDefaultInterfaceMethods:」

从seedMapper中获取所有的memberKey,计算memberKey对应的classNaming和type,获取type在library中的dexClass,如果存在则计算该type的mapping:

 DexClass dexClass = appView.appInfo().definitionForWithoutExistenceAssert(type);
      if (dexClass == null || dexClass.isClasspathClass()) {
        computeDefaultInterfaceMethodMappingsForType(
            type,
            classNaming,
            defaultInterfaceMethodImplementationNames);
      }

「MinifyClasses:」

前面的几个步骤是混淆前的准备工作,接下来是混淆的核心实现部分,首先是混淆所有的classes,核心类是ClassNameMinifier, 「renaming」保存的是最终需要mapping的映射,如果class在mapping中存在reverseName(即需要保留的Name)则存至renaming中,如果在mapping中找不到即新增的class,则计算新的computeName:

    for (ProgramOrClasspathClass clazz : classes) {
      if (!renaming.containsKey(clazz.getType())) { //如果是新增的class
        DexString renamed = computeName(clazz.getType());
        renaming.put(clazz.getType(), renamed);
        assert verifyMemberRenamingOfInnerClasses(clazz.asDexClass(), renamed);
      }
    }

「MinifyMethods:」

核心类为MethodNameMinifier,「renaming」 保存的是最终需要mapping的映射,主要实现细节主要分为以下几个阶段:

Phase1: reserveNamesInClasses

从object class开始遍历,计算所有class的reservationState, reservationStates保存的是 DexType到MethodReservationState的映射。如果能从super class中找到state,则直接获取,反之创建reservationState,reservationState保存的是当前的method的reverseName,即如果是 library,keep, 或者apply mapping中,则保留该reverseName:

    MethodReservationState<?> state =
        reservationStates.computeIfAbsent(frontier, ignore -> parent.createChild()); // 如果能从super中找到state,则get,反之创建
    DexClass holder = appView.definitionFor(type);
    if (holder != null) {
      for (DexEncodedMethod method : shuffleMethods(holder.methods(), appView.options())) {
        DexString reservedName = strategy.getReservedName(method, holder); // 如果当前的method 是 library,keep, 或者apply mapping中,则保留名字
        if (reservedName != null) {
          state.reserveName(reservedName, method);
        }
      }
    }
  }

Phase 2: reserveNamesInInterfaces

计算interfaceStateMap,它保存的是dextype与InterfaceReservationState的映射关系,minifierState 是一个工具类,可以辅助拿到renaming,reservationStates,frontiers(supTypes),strategy等信息

  private void reserveNamesInInterfaces(Iterable<DexClass> interfaces) {
    for (DexClass iface : interfaces) {
      assert iface.isInterface();
      minifierState.allocateReservationStateAndReserve(iface.type, iface.type);
      InterfaceReservationState iFaceState = new InterfaceReservationState(iface);
      iFaceState.addReservationType(iface.type);
      interfaceStateMap.put(iface.type, iFaceState);
    }
  }

Phase 3 : patchUpChildrenInReservationStates

计算interfaceStateMap所有的child节点

  private void patchUpChildrenInReservationStates() {
    for (Map.Entry<DexType, InterfaceReservationState> entry : interfaceStateMap.entrySet()) {
      for (DexType parent : entry.getValue().iface.interfaces.values) {
        InterfaceReservationState parentState = interfaceStateMap.get(parent);
        if (parentState != null) {
          parentState.children.add(entry.getKey());
        }
      }
    }
  }

Phase 4: computeReservationFrontiersForAllImplementingClasses

找出所有的subType并记录在InterfaceReservationState中:

    interfaces.forEach(
        iface ->
            subtypingInfo
                .subtypes(iface.getType())
                .forEach(
                    subType -> {
                      DexClass subClass = appView.contextIndependentDefinitionFor(subType);
                      if (subClass == null || subClass.isInterface()) {
                        return;
                      }
                      DexType frontierType = minifierState.getFrontier(subType);
                      if (minifierState.getReservationState(frontierType) == null) {
                        // The reservation state should already be added. If it does not exist
                        // it is because it is not reachable from the type hierarchy of program
                        // classes and we can therefore disregard this interface.
                        return;
                      }
                      InterfaceReservationState iState = interfaceStateMap.get(iface.getType());
                      if (iState != null) {
                        iState.addReservationType(frontierType);
                      }
                    }));

Phase 5:

遍历每个interface,并获取InterfaceReservationState, 再遍历每个iterface的方法,将method -> InterfaceReservationState 映射关系保存在 globalStateMap的methodStates中,其中globalStateMap 的key很关键,如果多个方法 对name 和paramter一样,则视为相同的key。

    for (DexClass iface : interfaces) {
      InterfaceReservationState inheritanceState = interfaceStateMap.get(iface.type);
      assert inheritanceState != null;
      for (DexEncodedMethod method : iface.methods()) {
        Wrapper<DexEncodedMethod> key = definitionEquivalence.wrap(method);
        globalStateMap
            .computeIfAbsent(key, k -> new InterfaceMethodGroupState())
            .addState(method, inheritanceState);
      }
    }

Phase 6:

拿到globalStateMap所有的key,即interfaceMethodGroups, 遍历每个key 为Wrapper, 获取key对应的InterfaceMethodGroupState, 计算该state的reversename,计算方式如下:

6.1 将methodStates的methodkey进行排序,并遍历每个DexEncodedMethod

6.2 获取DexEncodedMethod对应的InterfaceReservationState

6.3 如果这个method在InterfaceReservationState的reverseName也是它本身,则直接返回method.getName()

6.4 如果不是它本身,则取最后一个的reverseName, 如果reversename找不到则interfaceMethodGroup记录到nonReservedMethodGroups,找的到的话interfaceMethodGroup记录reservedName,记录原理:遍历reverseStates, 每个state为interfaceReservationState, 获取该state 的reservedname【如果子类存在reverseName, 则无法保留该interface name】, 最后将其保存到renaming中

//对于无法保留的interface Name(比如新增的interface method, 但是子类存在该Name), 则需要重新进行rename
    for (Wrapper<DexEncodedMethod> interfaceMethodGroup : nonReservedMethodGroups) {
      InterfaceMethodGroupState groupState = globalStateMap.get(interfaceMethodGroup);
      assert groupState != null;
      assert groupState.getReservedName() == null;
      DexString newName = assignNewName(interfaceMethodGroup.get(), groupState);
      assert newName != null;
      Set<String> loggingFilter = appView.options().extensiveInterfaceMethodMinifierLoggingFilter;
      if (!loggingFilter.isEmpty()) {
        Set<DexEncodedMethod> sourceMethods = groupState.methodStates.keySet();
        if (sourceMethods.stream()
            .map(DexEncodedMethod::toSourceString)
            .anyMatch(loggingFilter::contains)) {
          print(interfaceMethodGroup.get().getReference(), sourceMethods, System.out);
        }
      }
    }

Phase 7: assignNamesToClassesMethods

remapping method的核心逻辑,遍历app中的所有class, 并获取该class的 DexType,reservationState, namingState, 将其class的所有method进行排序遍历处理,对于每一个method, 首先计算它的reservedName:

DexString newName = strategy.getReservedName(method, holder);
    if (newName == null || newName == method.getName()) {
      newName = state.newOrReservedNameFor(method, minifierState, holder);
    }

如果reservedName 不存在或者为它的本身,则需计算它的assignedName,记为newName,如果newName存在则保存至renaming映射中, 如果为空继续获取该method的reservedNames, 如果reservedNames存在且size为1,将reservedName作为候选candidate:

 Set<DexString> reservedNamesFor = reservationState.getReservedNamesFor(method.getReference());
    // Reservations with applymapping can cause multiple reserved names added to the frontier. In
    // that case, the strategy will return the correct one.
    if (reservedNamesFor != null && reservedNamesFor.size() == 1) {
      DexString candidate = reservedNamesFor.iterator().next();
      if (isAvailable(candidate, method.getReference())) {
        return candidate;
      }
    } 

还需要继续计算该candidate的usedByMethod标记,即是否已被其他method remapping,如果usedByMethod存在且为method本身则candidate为最终的remapping值;

   Set<Wrapper<DexMethod>> usedBy = getUsedBy(candidate, method);
    if (usedBy != null && usedBy.contains(MethodSignatureEquivalence.get().wrap(method))) {
      return true;
    }

反之接着计算该candidata在method的reservationState状态,如果未存在reservation state 且 usedByMethod为空,则candidate为最终的remapping值;

    boolean isReserved = reservationState.isReserved(candidate, method);
    if (!isReserved && usedBy == null) {
      return true;
    }

反之通过reservationState拿到所有的reservationNames并判断该candidate是否有效:

    // We now have a reserved name. We therefore have to check if the reservation is
    // equal to candidate, otherwise the candidate is not available.
    Set<DexString> methodReservedNames = reservationState.getReservedNamesFor(method);
    boolean containsReserved = methodReservedNames != null && methodReservedNames.contains(candidate); 

「MinifyFields:」

实现细节跟Methods类似,这里不展开说明。

通过以上对算法的分析,回过头我们再来分析遇到的三类混淆问题:

「1. 类出现重复的相同方法,****Failure to verify dex file 'xxx==/base.apk': Out-of-order method_ids with applyMapping;」

「分析:」某次业务同学的改动,在applymapping安装包的启动阶段verify dex不通过导致启动crash,通过分析发现在mapping.txt存在不同method的相同映射,其中remeasure_new_new是新增方法:

  10:10:void remeasure():164:164 -> g
    11:11:void remeasure():167:167 -> g
    12:13:void remeasure_new_new():200:201 -> g
    14:14:void remeasure_new_new():209:209 -> g

原因是另个类TestView也存在相同的映射:

com.tencent.mm.TestView -> com.tencent.mm.TestView:
...
    1:1:int remeasure():157:157 -> e
    2:2:int remeasure():162:162 -> e
    3:3:int remeasure():164:164 -> e
    4:5:int remeasure():166:167 -> e
    6:8:int remeasure():169:171 -> e
    9:9:int remeasure():173:173 -> e
    10:10:int remeasure():176:176 -> e
    1:1:int remeasure_new():181:181 -> f
    2:2:int remeasure_new():186:186 -> f
    3:3:int remeasure_new():188:188 -> f
    4:5:int remeasure_new():190:191 -> f
    6:8:int remeasure_new():193:195 -> f
    9:9:int remeasure_new():197:197 -> f
    10:10:int remeasure_new():200:200 -> f
    1:1:void remeasure_new_new():205:205 -> g
    2:2:void remeasure_new_new():210:210 -> g
    3:3:void remeasure_new_new():212:212 -> g
    4:5:void remeasure_new_new():214:215 -> g
    6:8:void remeasure_new_new():217:219 -> g
    9:9:void remeasure_new_new():221:221 -> g
    10:10:void remeasure_new_new():224:224 -> g

而修改的类WheelView在base mapping中:

com.tencent.mm.WheelView -> com.tencent.mm.WheelView:
...
    1:1:void remeasure():148:148 -> g
    2:2:void measureTextHeight():179:179 -> g
    2:2:void remeasure():152 -> g
    3:3:void remeasure():153:153 -> g
    4:4:void remeasure():155:155 -> g
    5:6:void remeasure():157:158 -> g
    7:9:void remeasure():160:162 -> g
    10:10:void remeasure():164:164 -> g
    11:11:void remeasure():167:167 -> g

那么在WheelView添加新方法remeasure_new_new会命中R8的reservationName策略,即相同的method proto和name则会复用相同的reservationName,applymapping后也就会出现不同的method被映射为相同的method name,导致异常;回到R8源码定位到出问题的地方:

  boolean isAvailable(DexString candidate, DexMethod method) {
    Set<Wrapper<DexMethod>> usedBy = getUsedBy(candidate, method);
    if (usedBy != null && usedBy.contains(MethodSignatureEquivalence.get().wrap(method))) {
      return true;
    }
    boolean isReserved = reservationState.isReserved(candidate, method);
    if (!isReserved && usedBy == null) {
      return true;
    }
    // We now have a reserved name. We therefore have to check if the reservation is
    // equal to candidate, otherwise the candidate is not available.
    Set<DexString> methodReservedNames = reservationState.getReservedNamesFor(method);
    boolean containsReserved = methodReservedNames != null && methodReservedNames.contains(candidate);
    return containsReserved;
  }

判断candidate候选字段是否可用的条件判断时,忽略了一种可能性即containsReserved为True且usedBy不为null的情况,此时我们需要做兼容处理,防止出现candidate已经被该class的其他method所mapping。解决方案如下:

    if (containsReserved && usedBy != null) {
      for (Wrapper<DexMethod> methodWrapper : usedBy) {
        DexMethod usedDexMethod = methodWrapper.get();
        assert usedDexMethod != null;
        if (method.proto.equals(usedDexMethod.proto)) {
          System.out.print("Find containsReserved: \n");
          System.out.printf("------- usedBy: %s\n", usedDexMethod.toSourceString());
          System.out.printf("------- method: %s\n", method.toSourceString());
          return false;
        }
      }
    }

出现该问题的根本原因是R8存在horizontal/vertical merge优化,会将不同的类进行合并,我们也可以禁用这一优化来解决此类问题。

「2 接口方法找不到实现方法,java.lang.AbstractMethodError」

2.1 新增或者修改接口,导致call调用点crash,无法找不到实现类的方法, 例如某次构建出现:

在Base Mapping中的映射:

com.tencent.mm.loader.IRequestBuilder -> w10.b:
# {"id":"sourceFile","fileName":"IRequestBuilder.kt"}
    void load() -> a
    void into() -> b
    void into(android.widget.ImageView) -> c

com.tencent.mm.loader.builder.RequestBuilder -> x10.b:
    0:65535:com.tencent.mm.loader.IRequestBuilder setImageLoaderListener(com.tencent.mm.loader.listener.IImageLoaderListener):128:128 -> e

发现setImageLoaderListener方法在IRequestBuilder中被shrink掉,当业务同学再次修改IRequestBuilder这个接口,则applymapping后的结果出现了mapping不一致的现象:

com.tencent.mm.loader.IRequestBuilder -> w10.b:
# {"id":"sourceFile","fileName":"IRequestBuilder.kt"}
    void load() -> a
    com.tencent.mm.loader.IRequestBuilder setImageLoaderListener(com.tencent.mm.loader.listener.IImageLoaderListener) -> a
    void into() -> b
    void into(android.widget.ImageView) -> c


com.tencent.mm.loader.builder.RequestBuilder -> x10.b:
    0:65535:com.tencent.mm.loader.IRequestBuilder setImageLoaderListener(com.tencent.mm.loader.listener.IImageLoaderListener):128:128 -> e

setImageLoaderListener在接口被映射为a,而在实现类则被映射为e!原因是R8在method mapping时未考虑到两处条件下的判断处理:

针对这两种情况,我们分别做了兼容处理,解决了assignName被reservedName覆盖导致的问题,核心代码如下:

if (holder != null && reservedNamesFor != null && reservedNamesFor.size() > 1) {
      for (DexString candidate : reservedNamesFor) {
        if (isAvailableForInterface(candidate, holder, method, minifierState) && isAvailable(candidate, method.getReference())) {
           System.out.printf("Found multi reservedNames and match interface's candidate: %s, method holder: %s, method: %s\n", candidate.toString(), holder.getSimpleName(), method.getReference().toSourceString());
           return candidate;
        }
      }
    }
DexString assignedName = state.getAssignedName(method.getReference());
      if (assignedName != null && newName != assignedName && state.isAvailable(assignedName, method.getReference())) {
        System.out.printf("Found no same assigned and reserved name, method: %s, reservedName: %s, assignedName:%s\n", method.getReference().toSourceString(), newName, assignedName);
        newName = assignedName;
      } else {
        Set<DexString> reservedNamesFor = state.getReservedNamesFor(method.getReference());
        if (holder != null && reservedNamesFor != null && reservedNamesFor.size() > 1) {
          for (DexString candidate : reservedNamesFor) {
            if (state.isAvailableForInterface(candidate, holder, method, minifierState) && state.isAvailable(candidate, method.getReference())) {
              System.out.printf("Found multi reservedNames and match interface's candidate: %s, reservedName: %s, method holder: %s, method: %s\n", candidate.toString(), newName, holder.getSimpleName(), method.getReference().toSourceString());
              newName = candidate;
              break;
            }
          }
        }
      }
2.2 新增keep规则,interface method被keep住,但实现类的方法未被keep, 例如业务同学添加规则:
keep class com.tencent.thumbplayer.** {*;}
-keep interface com.tencent.thumbplayer.** {*;}
-keep class com.tencent.tvkbeacon.** {*;}
-keep class com.tencent.tvkqmsp.** {*;}

但mapping中依然存在未被keep的映射:

com.tencent.thumbplayer.tplayer.plugins.TPPluginManager -> com.tencent.thumbplayer.tplayer.plugins.TPPluginManager:
    1:3:void <init>():18:18 -> <init>
    4:11:void <init>():19:19 -> <init>
    1:4:void onEvent(int,int,int,java.lang.String,java.lang.Object):52:52 -> a
    5:8:void onEvent(int,int,int,java.lang.String,java.lang.Object):53:53 -> a
    9:14:void onEvent(int,int,int,java.lang.String,java.lang.Object):54:54 -> a
    15:28:void onEvent(int,int,int,java.lang.String,java.lang.Object):55:55 -> a
    29:33:void onEvent(int,int,int,java.lang.String,java.lang.Object):57:57 -> a
    1:5:void addPlugin(com.tencent.thumbplayer.tplayer.plugins.ITPPluginBase):25:25 -> addPlugin
    6:11:void addPlugin(com.tencent.thumbplayer.tplayer.plugins.ITPPluginBase):28:28 -> addPlugin
    12:14:void addPlugin(com.tencent.thumbplayer.tplayer.plugins.ITPPluginBase):29:29 -> addPlugin
    15:20:void addPlugin(com.tencent.thumbplayer.tplayer.plugins.ITPPluginBase):30:30 -> addPlugin
    1:4:void release():65:65 -> release
    5:8:void release():66:66 -> release
    9:14:void release():67:67 -> release
    15:22:void release():68:68 -> release
    23:26:void release():70:70 -> release
    27:32:void release():73:73 -> release
    33:35:void release():75:75 -> release
    1:4:void removePlugin(com.tencent.thumbplayer.tplayer.plugins.ITPPluginBase):36:36 -> removePlugin
    5:7:void removePlugin(com.tencent.thumbplayer.tplayer.plugins.ITPPluginBase):37:37 -> removePlugin

R8针对这一情况根本未做处理,因此在处理method mapping环节我们额外添加keepNames变量记录需要被keep的method,处理的最后再一次filterAdditionalMethodNamings即可:

if (!appView.appInfo().isMinificationAllowed(method.getReference())) {
      keepRenaming.put(method.getReference(), newName);
    }
  private void filterAdditionalMethodNamings(Map<DexMethod, DexString> keepRenaming) {
    additionalMethodNamings.replaceAll(keepRenaming::getOrDefault);
  } 
    methodRenaming.renaming.putAll(defaultInterfaceMethodImplementationNames);

「3 内部类access访问受限,java.lang.IllegalAccessError: Illegal class access:****」

「分析:」例如某次构建出现一次crash异常:

com.tencent.mm.feature.performance.PerformanceFeatureService$$ExternalSyntheticLambda6 -> com.tencent.mm.feature.performance.g:

R8在处理匿名内部类时,会考虑outerClass的mapping规则,保证内部类和外部类package保持一致,但是上面的crash所在类严格来说也是一种innerClass,它是从lambda desugar而来,但R8未处理此情况,我们做了一些此case的兼容处理:

 if (appView.app().options.getProguardConfiguration().hasApplyMappingFile()) {
        DexClass clazz = appView.definitionFor(type);
        if (clazz instanceof DexProgramClass && SyntheticNaming.isSynthetic(clazz.getClassReference(), SyntheticNaming.Phase.EXTERNAL, new SyntheticNaming().LAMBDA)) {
          String prefixForExternalSyntheticType = new SyntheticProgramClassDefinition(new SyntheticNaming.SyntheticClassKind(1""false), null, (DexProgramClass) clazz).getPrefixForExternalSyntheticType();
          DexType outerClazzType = appView.dexItemFactory().createType(DescriptorUtils.javaTypeToDescriptor((prefixForExternalSyntheticType)));
          state = getStateForOuterClass(outerClazzType, SyntheticNaming.SYNTHETIC_CLASS_SEPARATOR);
          System.out.printf("Found synthetic clazz: %s\n, renaming", clazz.toSourceString());
        }
      }

「DexLayout:」

我们知道 Android 中的 65535 问题,dex 文件中的 type_idsfield_idsmethod_ids 等索引数量不允许超过 65535 个,任何一个达到上限的时候,就会触发分 dex 条件把 class 分到下一个 dex 中,并且每次构建不能保证在代码发生变化之后 class 在 dex 中的顺序,这可能会对我们 patch 包的大小造成一定影响。因此早在几年前我们有了AutoDex方案足以生成最小的dex数量和生成最小的patch,但接入R8之后AutoDex已不再适用,为此我们基于Redex的Interdex方案将c代码java实现并重新优化了dex的排列方式,使得在R8我们同样能够支持dex重排。具体的核心工作内容除了移植interdex的算法本身,我们还利用dexlib2库分析了每一个DexClazz的methods、field和types,这是实现该算法的必要前提:

     ...
method.implementation?.instructions?.forEach { _insn ->
    val opcode = _insn.opcode
    when (opcode.referenceType) {
        // Method
        ReferenceType.METHOD -> {
            //https://github.com/facebook/redex/blob/90762687fb33c89fd2eafd03738c90590dbfb7c3/libredex/DexInstruction.cpp
            val methodReference = (_insn as ReferenceInstruction).reference as MethodReference
            gatherMethod(dexClassNode, method, methodReference)
        }

        ReferenceType.METHOD_HANDLE -> {
            gatherMethodHandler(dexClassNode, method, (_insn as MethodHandleReference))
        }

        // Field
        ReferenceType.FIELD -> {
            val fieldReference =
                (_insn as ReferenceInstruction).reference as FieldReference
            gatherField(dexClassNode, method, fieldReference)
        }
      ...
      ...
private fun resolveValue(dexClassNode: ResolverDexClassNode, encodedValue: EncodedValue) {
    when (encodedValue.valueType) {
        ValueType.BYTE -> {
            dexClassNode.annotationReferredTypes.add("B")
        }
        ValueType.BOOLEAN -> {
            dexClassNode.annotationReferredTypes.add("Z")
        }
        ValueType.SHORT -> {
            dexClassNode.annotationReferredTypes.add("S")
        }
        ValueType.INT -> {
            dexClassNode.annotationReferredTypes.add("I")
        }
        ValueType.LONG -> {
            dexClassNode.annotationReferredTypes.add("J")
        }
        ValueType.FLOAT -> {
            dexClassNode.annotationReferredTypes.add("F")
        }
        ValueType.DOUBLE -> {
            dexClassNode.annotationReferredTypes.add("D")
        }
        ValueType.STRING -> {
            dexClassNode.annotationReferredTypes.add("Ljava/lang/String;")
        }
      ...

最后

目前R8已经相对稳定运行在「Android微信的最新版本中」,且问题已基本收敛。同时在「包大小、低端机冷启动性能方面有不错的收益」,欢迎大家留言交流。

参考资料:https://r8.googlesource.com/r8


想了解更多「微信客户端技术及开发经验」,请关注「微信客户端技术团队公众号」


继续滑动看下一个
微信客户端技术团队
向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存