范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文
国学影视

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

  作者:chrispaul,来自微信客户端团队  背景
  在之前的版本,微信Android一直采用Proguard构建Release包,主要原因在于:  Proguard优化足够稳定  ApplyMapping也能保证正确性  与AutoDex搭配使用,生成足够小的Tinker Patch。
  但Proguard也有明显的不足之处:  Kotlin版本的升级与Proguard存在不兼容,导致被迫升级Proguard版本;  Proguard版本升级导致编译时间变慢,超过30min;  由于历史原因,一些keep规则导致包大小无法达到最优;  随着AGP的升级,将默认采用Google的R8来构建以获取更优的Apk性能;  R8的优势
  相对于Proguard,R8的优势在于:  能力支持:R8对Kotlin有更好的支持;  构建耗时:虽然我们有增量Proguard编译,但在全量构建时间R8比Proguard更短,开启优化只需要15min左右,比Proguard缩短至少一半的构建时间;  开启R8优化,使得将应用程序减少了至少14M的包大小优化,这个是我们切换R8的主要原因;  Apk构建流程❝
  AGP 7.2.2 Gradle 7.5
  ❞  1. 使用Proguard构建
  图1
  说明:  Proguard生成优化的java字节码,包括提供混淆代码能力;  在打Patch apk时,利用Proguard的ApplyMapping能力保证前后构建的代码混淆结果一致;  AutoDex确保将前后构建的dalvik字节码分布在相同的dex序列中,为了生成尽可能小的tinker patch;  2. 开启R8后
  图2
  可见R8省去了dex环节直接将java字节码生成dalvik字节码,由于在Android微信我们大部分发版都是基于Tinker patch的方式进行的,因此接入R8之后必须提供applymapping、autodex的类似能力(图3),使得打出更小的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(图4),导致业务通过getField反射获取字段出现错误的返回,解决的办法可以通过-allowaccessmodification来规避,或者修改子类的access改为public等方式
  图4
  「2. InvokeDynamic指令导致类合并」
  「分析:」 业务有的地方会对一些类做一些check(图6),比如检查传入的class是否存在默认构造函数(())
  通过crash我们查看字节码发现,kotlin隐式调用接口,会生成 visitInvokeDynamic指令; 给到R8, 会将多个调用的对象进行合并到一个类;而kotlin显式调用接口,会编译生成匿名内部类,给到R8, 不会将多个调用的对象进行合并为一个类;解决此问题我们采用了取巧的方案:为了不让kotlinc生成invoke-dynamic,在kotlinc阶段添加 "-Xsam-conversions=class", 这样就没有 method handler 和callsite class,从而R8就没有机会做类合并;
  图7
  「3. 强引用的Field变量被shrink」
  图8
  「分析:」 如上图所示,业务刻意通过赋值给强引用变量来防止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差异较大:
  图9
  可能的解决方案主要有三种:  删除debugInfo,但势必增加还原crash轨迹的难度,增加开发成本;  applymapping阶段复用上次行号优化的结果,改动较大,不利于长期维护;  了解到R8在优化行信息时,R8 现在可以使用基于指令偏移量的行表来对共享调试信息对象中的信息进行编码。这可以显著减少行信息的开销。从API级别 26开始的 Android 虚拟机支持在堆栈轨迹中输出指令偏移量(如果方法没有行号信息)。如果使用minsdk 26 或更高版本进行编译,并且没有源文件信息,R8 会完全删除行号信息。
  为此我们采用了方案3的解决思路,也是顺应了未来低端机型不断被淘汰的大趋势,将R8的行号优化改为基于指令偏移量的行表的方式:
  图10
  「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 ():173:173 ->      8:15:void ():175:175 ->      0:65535:void ():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左右的包大小损失:
  [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BK7N7oMU-1675673554351)(/Users/wuzhengshan/Downloads/R8-12.webp)]  「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
  [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-kdXDYPrq-1675673554351)(/Users/wuzhengshan/Downloads/R8-13.webp)]  「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的混淆流程图大致如下:
  [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-j8T62tBq-1675673554351)(/Users/wuzhengshan/Downloads/R8_3.drawio.svg)]
  「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 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 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 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 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 interfaceMethodGroup : nonReservedMethodGroups) {       InterfaceMethodGroupState groupState = globalStateMap.get(interfaceMethodGroup);       assert groupState != null;       assert groupState.getReservedName() == null;       DexString newName = assignNewName(interfaceMethodGroup.get(), groupState);       assert newName != null;       Set loggingFilter = appView.options().extensiveInterfaceMethodMinifierLoggingFilter;       if (!loggingFilter.isEmpty()) {         Set 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 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> 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 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> 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 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 methodWrapper : usedBy) {         DexMethod usedDexMethod = methodWrapper.get();         assert usedDexMethod != null;         if (method.proto.equals(usedDexMethod.proto)) {           System.out.print("Find containsReserved:  ");           System.out.printf("------- usedBy: %s ", usedDexMethod.toSourceString());           System.out.printf("------- method: %s ", method.toSourceString());           return false;         }       }     }
  出现该问题的根本原因是R8存在horizontal/vertical merge优化,会将不同的类进行合并,我们也可以禁用这一优化来解决此类问题。
  「2 接口方法找不到实现方法,java.lang.AbstractMethodError」
  2.1 新增或者修改接口,导致call调用点crash,无法找不到实现类的方法, 例如某次构建出现:
  [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IAdNxv6V-1675673554352)(/Users/wuzhengshan/Downloads/R8-14.webp)]
  在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时未考虑到两处条件下的判断处理:
  [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-67cbIJTB-1675673554352)(/Users/wuzhengshan/Downloads/R8-15.webp)]
  [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b0v4zBlr-1675673554352)(/Users/wuzhengshan/Downloads/R8-16.webp)]
  针对这两种情况,我们分别做了兼容处理,解决了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 ", 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 ", method.getReference().toSourceString(), newName, assignedName);         newName = assignedName;       } else {         Set 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 ", 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 ():18:18 ->      4:11:void ():19:19 ->      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 keepRenaming) {     additionalMethodNamings.replaceAll(keepRenaming::getOrDefault);   }      methodRenaming.renaming.putAll(defaultInterfaceMethodImplementationNames);
  「3 内部类access访问受限,java.lang.IllegalAccessError: Illegal class access:****」
  「分析:」 例如某次构建出现一次crash异常:
  [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HGmF7vcS-1675673554353)(/Users/wuzhengshan/Downloads/R8-17.webp)]  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 , renaming", clazz.toSourceString());         }       } 「DexLayout:」
  我们知道 Android 中的 65535 问题,dex 文件中的  type_ids 、field_ids 、method_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
  作者:chrispaul
  来源:微信公众号:微信客户端技术团队
  出处:https://mp.weixin.qq.com/s/qXgGO9m3_VFOfiJ9rw3hoQ

太仓浏河镇再添一处网红打卡地,总投资3亿元打造乡村文旅新业态2月17日,七十二家理想村一个地球自然基金会湿地项目签约仪式暨树蛙部落民宿开业仪式举行。七十二家村理想村位于长江入海口的江苏太仓浏河镇,项目总投资3亿元,总规划面积约4400亩,核逐梦冰雪的起点43年前,普莱西德湖畔的中国红1980年2月,第十三届冬季奥林匹克运动会在美国普莱西德湖举行。这是新中国历史上第一次参加奥运会,也是逐梦冰雪的起点。整整43年过去,中国冰雪运动如今已经取得了长足发展,并在冬奥赛四大位置对比男篮PK杜锋带领的广东队,客观分析谁更占优势话不多说,来看双方四大位置的全方面对比1。主教练乔尔科维奇VS杜锋(杜锋完胜)笔者这边坚定认为主教练这边杜锋优势最大。杜锋执教风格灵活多变,崇尚全场紧逼而乔帅战术讲究团队合作尤其强中国足坛新瓜?网传33岁老将留洋与陈戌源有关,其真相不攻自破中国足坛新瓜?网传33岁老将留洋与陈戌源有关,其真相不攻自破!就在李铁和陈戌源相继落马之后,中国足坛逐渐呈现出天朗气清之势,再加之有人民日报的言辞激励,相信中国足球很快就会回到正轨儿童抽动障碍春季到来,关注那些爱眨眼的神兽春天来临,神兽们也迎来了新的学期,许多家长对孩子的学校生活学习成绩重新关注了起来,近日门诊上爱眨眼的小神兽逐渐增多,这是怎么一回事呢?让我们共同了解一下这个病。抽动障碍抽动症是抽动破解建筑节能痛点发布三大创新成果海尔智慧楼宇竞逐双碳风口本报记者方超石英婧上海报道双碳风口之下,智慧楼宇正成为助力节能降碳的重要切入口。2月16日,2023海尔智慧楼宇成果暨新品发布会在青岛召开,针对行业顽症与用户难题,海尔智慧楼宇发布春季雨水养生记得喝这三种粥,吃这两种菜,让你身体倍儿棒我是饮食健康管理师小辉。雨水时节来了,虽然春季养生法则差不多,但这一期我们重点给出一些饮食食谱和效果,十分实用。春季推荐喝这三种粥。1胡萝卜南瓜小米粥胡萝卜南瓜小米都是黄色食物,属苹果推出iOS16。4beta1版系统正式版将于春季发布前不久苹果推出了iOS16。3。1正式版系统,现在内测版系统已更新到了iOS16。4beta1版本,这个版本主要变化是新加入了一些emoji表情,支持了Safari浏览器推送通知消鬼怪女主的春日单品推荐孤独又灿烂的神这部剧中,女主金高银的穿搭收获了众多网友的称赞。其中,女主的许多春日单品尤为经典,它们不仅舒适实用,更是为女主增添了不少青春活力的气质。下面就来盘点一下有哪些单品。1广州老板的白酒单爆火,老酒友这才是普通人喝的,款款纯粮众所周知,我国的东部沿海城市都是经济发展快,繁华的地区,广州的老板大部分都是有钱的人,有钱的人普遍都会给人一种他们在吃穿住行上都是高品质的要求。广州老板的餐桌上有山珍海味,那必定少春日里的中国春雪落谷迎客来红崖赤壁景壮美天山托木尔景区(央广网发杨礼文摄)央广网温宿2月19日消息(记者李昊轩通讯员郭海涛)近日,一场春后雪降在新疆阿克苏地区温宿县天山托木尔景区,景区中的大峡谷迎来了春日里最美的时刻。红
ASP。NET题库管理系统源码生成word格式试卷VS2010CSqlServer2008R2CS架构一功能介绍1自定义试题库管理系统目录难易程度,题型,知识库等。2试题录入。3强大的试题编辑功能,并与通常应用编辑工具有共通。4灵不是我吹,华为音乐正在为生活加料变更甜原谅中年男人对音乐不敏感,这么久才发现一个很好用的听歌工具!事情是这样的,前几天在朋友车上无意发现他的音乐都很戳中我心,后来才知道是华为音乐的功劳。身为一个技术类博主,竟然对这方面2023年筋膜枪测评,耗时30天6000字长文教你避雷!从大四实习到现在做家电测评工作好几年,工作原因所以我几乎试用测评过市面上所有热门的筋膜枪型号。说实话市面上产品水平层次不齐,存在部分代工生产的产品,其击打精准度和稳定性都有待提升,闲鱼太乱了,没有能力的小伙伴慎入我在闲鱼上本来是买8g内存的,8g内存闲鱼价格80左右,4g内存价格45左右,并且很少有人要。遇到这个闲鱼卖家发布内存商品,只写繁杂难懂的型号参数84,不标明通俗易懂的容量大小,并中医专家教您如何对抗咽痛发热清热治瘟毒对抗咽痛发热近期很多朋友出现咽痛发热等症状,在冬季容易发作,如何缓解咽痛发热,中医科王阳主任为大家支招。中医专家教您如何对抗咽痛发热中医古籍中有关疫毒的记载很多。也称作时伊林俄罗斯美女艺人赴欧洲旅游,拍写真桌历人生第一次出国伊林俄籍美女艺人安妮安妮的写真桌历于12月30日上市。(伊林娱乐提供)伊林俄罗斯美女艺人安妮上月只身赴欧洲旅游,3周的旅程,足迹跨越英国法国义大利及土耳其4国,此行除旅行外,主要任厕所革命助推全域旅游发展森林木屋厕所绿色生态厕所石头房子厕所如今,从景区到全域,从城镇到乡村,一座座旅游公厕成为松潘县青山绿水间的一道道靓丽风景线。近年来,松潘县不断创新,探索文旅市场化发展新模式,大力提四川汶川迎首个冰雪季羌人谷滑雪场正式开板12月23日,记者从汶川县文化体育和旅游局获悉,位于阿坝州汶川县灞州镇的羌人谷滑雪场正式开板。据了解,本次活动意在发挥冰雪体育的基础作用,以此借文化产业赋能乡村振兴之力,激发文旅融海南岛国际电影节户外展映环节受欢迎海南日报三亚12月22日电(记者李艳玫张期望)面朝大海,在海风吹拂中,看一场电影是什么感受呢?连日来,在第四届海南岛国际电影节户外电影展映环节,海口三亚的不少市民游客有了亲身体会。问道东关,漫步廊桥,泉州永春又一次惊艳到我了听说闽南也有长廊屋盖廊桥?是啦,就在泉州永春的东关镇。最主要还是因为莫兰蒂这个超强台风,那一年就隐隐地听到了泉州有个古老的廊桥被部分损毁,今天总算是亲眼所见,在这之前我看到的都是浙六万秦军命丧阏与之战,赵括之父赵奢一战成名因北方强赵的存在,使秦对兼并韩魏有所顾忌,遂寻机打击赵国。公元前273年,秦攻取赵地3城后,赵以公子部为质于秦,并与秦签订以焦魏牛狐交换3城的协议。后又反悔。公元前269年秦昭襄王