作者

大家好,我叫小嘉; 自己20年本科毕业于广东工业大学,于2020年6月参加37手游安卓团队;现在作业是国内游戏发行安卓相关开发。

本文目录

一、布景
二、剖析及解决
1、multidex 相关介绍
2、分主dex调研
(1)gradle 分主dex要害代码
(2)gradle 3.0 之后代码改变
3、测验(Android Gradle Plugin 3.3.0 版别)
(1)mainfest_keep.txt 规矩检测
(2)maindexlist.txt 规矩检测
三、分 dex 计划(gradle 3.3.0 版别)
四、实战

一、布景

在游戏发行职业,现在咱们关于多个渠道sdk 都需求接同一个游戏包的事务场景下,现在采用的计划是: 用 apktool 反编译游戏包 跟 用 apktool 反编译渠道sdk demo资源兼并后从头回编译打成 apk 包 。

可是,有时代码量较多,在进行回编译 smali 文件夹有时分会呈现 办法数超 65535 的过错,咱们便打不了包。关于办法数超的问题,谷歌推出了官方的解决计划: multiDex。

难点

multidex 的触发机遇是在回编译时 gradle 插件触发的,但无法适用于咱们现在的事务场景,为了兼容低版别手机,不能采取随意分包的办法。

二、剖析及解决

multidex 相关介绍

multidex 运用的官方文档: developer.android.google.cn/studio/buil… 在 art 虚拟机中,也便是 Android 5.0 版别之后,官方现已主动支撑 multiDex 操作,因为 art 虚拟机运转的的 oat 文件现已把多个 dex 文件集合在一个 oat 文件了。 但为了兼容低版别手机,需求针对 Multidex 进行装备:

1 在项目的 build.gradle 文件里边设置 multiDexEnabled 为true
2 增加关于 multidex 的依靠,清单文件中声明的 Application(二选一) :
(1) 承继 MultiDexApplication.
(2)在 attachBaseContext() 中调用 Multidex.install().

关于进程 1 而言,是 android 的 gradle 插件所做的,使其每一个dex 办法数目不会超越 65535,并且能兼容低版别手机。

关于进程 2 而言, 依靠的是一个 jar 包,后面所做的进程其实便是怎样让多个 dex 能在 davilk 虚拟机上运转。其实便是反射了一个 dexElements, 将其他 dex 的途径放在这个 dexElements 里边,这时 classloader 在进行加载 class 的时分便不会找不到类。

原理可查看相关博客: Android分包MultiDex源码剖析 – HansChen – 博客园 (cnblogs.com)

剖析

所以为了兼容低版别手机,咱们需求做的是:

1 拷贝 google 官方履行 multideDexEnadble 这个进程,即 google 是怎样分包的?
2 履行 multidex 的处理,增加 multidex 支撑库,以及相应的处理操作。
为什么需求分主 dex 和其他dex,主dex 和其他 dex 有什么区别吗?

multidex.install() 办法是在 application 的 attachBaseContext 里履行,假设某些类没有在 主 dex , 可是履行机遇在 application 的 attachBaseContext 之前,那这个时分便是出错了,所以一些特定的类需求放在 主 dex。

分主dex调研:

网上的有许多 gradle 分主 dex 相关的博客解析:

www.jianshu.com/p/27319854c…

/post/684490…

/post/684490…

gradle 分主dex要害代码:

public void transform(TransformInvocation invocation) throws IOException, TransformException, InterruptedException {
    LoggingManager loggingManager = invocation.getContext().getLogging();
    loggingManager.captureStandardOutput(LogLevel.INFO);
    loggingManager.captureStandardError(LogLevel.WARN);
    try {
        File input = verifyInputs(invocation.getReferencedInputs());
        this.shrinkWithProguard(input);
        this.computeList(input);
    } catch (ProcessException | ParseException var4) {
        throw new TransformException(var4);
    }
}
private void shrinkWithProguard(File input) throws IOException, ParseException {
        this.configuration.obfuscate = false;
        this.configuration.optimize = false;
        this.configuration.preverify = false;
        this.dontwarn();
        this.dontnote();
        this.forceprocessing();
        this.applyConfigurationFile(this.manifestKeepListProguardFile);
        if (this.userMainDexKeepProguard != null) {
            this.applyConfigurationFile(this.userMainDexKeepProguard);
        }
        this.keep("public class * extends android.app.Instrumentation { <init>(); }");
        this.keep("public class * extends android.app.Application {   <init>();   void attachBaseContext(android.content.Context);}");
        this.keep("public class * extends android.app.backup.BackupAgent { <init>(); }");
        this.keep("public class * extends java.lang.annotation.Annotation { *;}");
        this.keep("class com.android.tools.ir.** {*;}");
        this.libraryJar(this.findShrinkedAndroidJar());
        this.inJar(input, (List)null);
        this.outJar(this.variantScope.getProguardComponentsJarFile());
        this.printconfiguration(this.configFileOut);
        this.runProguard();
    }

分主 dex 的进程主要由 shrinkWithProguard() 和 computeList() 构成。

从 shrinkWithProguard() 的代码中看出:

1.找出 manifest_keep.txt,multidex-config.txt (自界说装备), multidex-config.pro (自界说装备) 中的类。

2.找到 Instrumentation , Application ,BackupAgent , Annotation

对这些类调用 shrinkedAndroid.jar 包 解析得到一些类。

computeList() 用来找出类的引证。

multidex-config.txt 和 multidex-config.pro 的因由(来自 multidex 运用的官方文档):

安卓游戏发行-打包 65535 方法数超?来个自动分多 dex
安卓游戏发行-打包 65535 方法数超?来个自动分多 dex

在 gradle2.2.4 版别中, CreateManifestKeepList 是用来生成 manifest_keep.txt 文件的。这个是用来解析 AndroidMainfest 清单文件的,其间最显着的一条是:粗心便是用来解析四大组件。

private static final Map<String, String> KEEP_SPECS = ImmutableMap.builder().put("application", "{\n    <init>();\n    void attachBaseContext(android.content.Context);\n}").put("activity", "{ <init>(); }").put("service", "{ <init>(); }").put("receiver", "{ <init>(); }").put("provider", "{ <init>(); }").put("instrumentation", "{ <init>(); }").build();

gradle 3.0 之后代码改变

但在 gradle 3.0 版别之后, CreateManifestKeepList 这个类不见了,阐明 Android Gradle Plugin 在 分多 dex 战略上也是有在一向改变的。

关于 shrinkWithProguard() 和 computeList()相关代码中,好像许多也很杂,直接看源码好像不是一个好选择。可是咱们能够经过一些测验来找到规矩从而找到 multidex 规矩。

以咱们现在日常工程用的 Android Gradle Plugin 3.3.0 版别,咱们来看一下到时是怎样分多 dex 咱们装备了 multidex 后,进行 build 指令后看到工程目录下 build 文件夹有 multi-dex 这个目录,其间存放着 manifest_keep.txt 目录,和 maindexlist 目录。

manifest_keep.txt: 对 AndroidMainfest 的剪裁,保存需求 keep 的部分。
maindexlist.txt: 需求放在主 dex 的一切类,存放的是类的途径。

安卓游戏发行-打包 65535 方法数超?来个自动分多 dex

maindexlist.txt 中的类是怎样来的?

输入这些需求 keep 住的类后,调用了 shrinkedAndroid.jar,做了什么呢?computeList()又做了什么?调用了 shrinkedAndroid.jar + computeList() 做了什么才会算进 maindexlist.txt 中?因为 gradle 源码比较复杂且 调用了相关 jar 包,没办法直接从源码上看整个的流程逻辑,但能够从咱们自己测验中去看成果。

测验(Android Gradle Plugin 3.3.0 版别)

mainfest_keep.txt 规矩检测:

此时咱们在清单文件里边声明四大组件,是否会放在 manifest_keep 里边呢?

安卓游戏发行-打包 65535 方法数超?来个自动分多 dex

运转成果:

安卓游戏发行-打包 65535 方法数超?来个自动分多 dex

发现只要 Application 会存放在 mainfest_keep.txt 中。 其实也能够理解: 调用流程: Application的attachBaseContext —> ContentProvider的onCreate —-> Application的onCreate —> Activity、Service等的onCreate(Activity和Service不分先后); 关于履行次序,具体可看博客:

blog.csdn.net/long117long…

maindexlist.txt 规矩检测:

依据前面的定论可得出 keep 规矩的源头有以下:
1 mainfest_keep.txt
2 multidex-config.txt (自界说装备)
3 multidex-config.pro (自界说装备)
4 android.app.Instrumentation
5 android.app.Application
6 android.app.backup.BackupAgent
7 java.lang.annotation.Annotation

下面以 keep public class * extends java.lang.annotation.Annotation { *;} 为比如来看一下有哪些类会放在 maindexlist.txt 中:
咱们需求验证六个点:
1 keep 的类 中 匿名内部类是否会算进去?
2 keep 的类中的直接引证是否会算进去?
3 keep 的类中的直接引证是否会算进去(即keep 类中一切引证的引证)?
4 keep 的子类中是否会算进去?
5 keep 的子类中 1 2 3 点是否会算进去?
6 关于其他类引证了该类是否会算进去?

验证代码:

//自界说注解
public @interface MyAnnation {
}
// 完成类
public class ImplementMyAnnation implements MyAnnation{
    @Override
    public Class<? extends Annotation> annotationType() {
        return null;
    }
//匿名内部类  验证第1点
     View.OnClickListener a = new View.OnClickListener() {
         @Override
         public void onClick(View v) {
             int a =10 ;
         }
     };
    ImplementMyAnnationRefClass implementMyAnnationRefClass = new ImplementMyAnnationRefClass();
}
// 直接引证类 验证第2点
public class ImplementMyAnnationRefClass {
    ImplementMyAnnationRefRefClass implementMyAnnationRefRefClass = new ImplementMyAnnationRefRefClass();
}
// 直接引证类 验证第 3点
public class ImplementMyAnnationRefRefClass {
    ImplementMyAnnationRefRefRefClass implementMyAnnationRefRefRefClass = new ImplementMyAnnationRefRefRefClass();
}
// 直接引证类 验证第 3点
public class ImplementMyAnnationRefRefRefClass {
}
// 子类 验证第 4点
public class ExtendImplementMyAnnation extends ImplementMyAnnation {
//匿名内部类 验证第 5点
    View.OnClickListener a = new View.OnClickListener() {
        @Override
        public void onClick(View v) {
            int a =10 ;
        }
    };
    ExtendImplementMyAnnationRefClass extendImplementMyAnnationRefClass = new ExtendImplementMyAnnationRefClass();
}
// 子类直接引证类 验证第 5点
public class ExtendImplementMyAnnationRefClass {
    ExtendImplementMyAnnationRefRefClass extendImplementMyAnnationRefRefClass = new ExtendImplementMyAnnationRefRefClass();
}
// 子类直接引证该类 验证第 5点
public class ExtendImplementMyAnnationRefRefClass {
}
//其他类引证了 ImplementMyAnnation  验证第 6点
public class ClassRefImplementMyAnnation {
    ImplementMyAnnation implementMyAnnation = new ImplementMyAnnation();
}

运转后在 maindexlist 中的内容为:

安卓游戏发行-打包 65535 方法数超?来个自动分多 dex

可得出定论(还验证了一些其他的类,没贴出来):

1 keep 的类 中 匿名内部类会算进去。
2 keep 的类中的直接引证是会算进去。
3 keep 的类中的直接引证是会算进去(即keep 类中一切引证的引证)。
4 keep 的子类中是会算进去。
5 keep 的子类中 1 2 3 点是会算进去。
6 关于其他类引证了该类是不会算进去。

注:Annation 的用法比较特殊(分自界说注解类,完成注解类,承继注解类):关于规矩也都适宜

三、分 dex 计划(gradle 3.3.0 版别)

1 找出 清单文件中的 MainApplication 类。
2 找出 multidex-config.txt (自界说装备), multidex-config.pro (自界说装备) 中需求的类
3 找出来 android.app.Instrumentation , android.app.Application,android.app.backup.BackupAgent ,java.lang.annotation.Annotation 这些类
对 1 2 3中的类别离履行操作:
(1) 找出各个类的一切引证(递归一切引证) 参加到 maindexlist
(2) 找出一切子类,同时找出各个子类的一切引证(递归一切引证) 参加到 maindexlist
(3) 找出各个类的匿名内部类参加 参加到 maindexlist

四、实战

因为作业性质决议,需求对 apk 进行反编译操作,从中心加一些代码进去,有时会导致 65535 问题,所以multidex 操作 是在 smali 层进行。

放入 maindexlist 中的类

1 界说 subClassList 中的类:
(1) 清单文件中的Application, 主 Activity, provider 标签的类 (触发机遇比较早)
(2) android.app.Instrumentation , android.app.Application , android.app.backup.BackupAgent, java.lang.annotation.Annotation 类
2 获取 subClassList 中的直接引证类和依靠引证类放进 maindexlist 类中
3 获取 subClassList 中一切子类的直接引证类和依靠引证类放进 maindexlist 类中
4 对 maindexlist 中类 存在的的匿名内部类放进 maindexlist 中。
注:(multidex-config.txt , multidex-config.pro 中没有去找,很少需求自界说装备)

最重要的是如何取得一个 类的一切引证,这儿介绍一个很好用的工具:Javassist Javassist 是一个开源的剖析、修改和创建Java字节码的类库。 里边的 api 用法可详看: www.javassist.org/html/javass…
其间有一个 getRefClasses() 办法 能够获取该类的一切引证。大概是原理是解析了 class 文件信息,从而获取相应信息,有爱好了解 class 文件的同学自行学习。

主要进程

(1) 因为 javassist 的操作对象是 class 文件或者 jar 包,所以需求进即将 smali 转为 jar 包。 这儿采用的路线是: smali –> dex –>jar ,别离用到工具 smali.jar 和 dex2jar 这个两个工具。
注:smali.jar 是用来将 smali 转为 dex 的,可是 假设 smali 文件夹 办法数超 65535 ,则会失利,所以可分多几个 smali 文件夹来确认每一个包都不会超越 65535 。
在处理 smali 文件进程中,如何确认是否能够还是比较每一个 smali 文件夹中办法数目不会超越 65535?这儿试了许屡次的之后,发现 smali 文件数目不超越 6000 的话,是一个很安全的规模,通常状况下都能打成 dex 成功。 所以这儿写了一个 计算文件数目的办法 + 对每个文件进行数目平分后移动,并循环操作到 每个文件夹中 smali 文件数目 都不会超越 6000。

/**
 * 将 path 中 smali 文件夹 超越 num 数量 的 smali 文件夹进行平分操作
 * @param path
 */
   public static void splitSmali(String path,int num){
   System.out.println("splitSmali:---> " + "path: " + path);
   File file = new File(path);
   int originalCount = 0;
   int smaliCount = 0;
   for (File f:file.listFiles()){
       if (f.getName().startsWith("smali"))  // 判别当时目录下有几个 smali 文件夹
           originalCount ++ ;
   }
   smaliCount = originalCount;
   System.out.println("originalCount: " + originalCount);
   Comparator<Map.Entry<String, Integer>> valCmp = new Comparator<Map.Entry<String,Integer>>() {
       @Override
       public int compare(Map.Entry<String, Integer> o1, Map.Entry<String, Integer> o2) {
           return o2.getValue()-o1.getValue();
       }
   };
   for (int i=1;i<=originalCount;i++){
        String suffix = i==1?"smali":"smali_classes" + i;
        String smaliPath = path + File.separator + suffix;
        System.out.println("smaliPath:--> " + smaliPath);
        ArrayList<String> list = averageSmali(smaliPath,valCmp,num); // 平分操作,假如当时 smali 文件夹 数目操作,则进行平分操作,返回需求移			动的文件夹  
        if (list!=null){
            smaliCount ++;
            for (String originalPath: list){
                String targetPath = originalPath.replaceAll(suffix,"smali_classes" + smaliCount);// 给移动后的文件夹命名
                File targetFile = new File(targetPath);
                File originalFile = new File(originalPath);
                FileUtil.checkParentFile(targetPath);// 假如没有父目录就创建父目录
                boolean isMoveSuccess =  originalFile.renameTo(targetFile);
                if (isMoveSuccess){
                    System.out.println("originalPath:" + originalPath + " 移动到: " + targetPath);
                }
            }
        }
    }
    File file1 = new File(path);
    for (File f1:file1.listFiles()) {
        System.out.println(f1.getAbsolutePath());
        if (f1.getName().startsWith("smali")) {
            File srcFile = new File(f1.getAbsolutePath());
            int sum = FileUtil.getFilesCount(srcFile, "smali");
            System.out.println("sum" + "-" + sum);
            if (sum>6000){
                System.out.println("包太大,需求从头分包!!");
                splitSmali(path,num);  // 假如当时还有 smali 文件夹文件数目超越 6000,则再进行切割操作。
            }
        }
    }
 }

(2) javassist 在设置 classLoader 的时分,需求把 android.jar 包放进去,并将用 dex2jar 生成的 jar 包也放进去. (android.jar 包是 虚拟机自带的,可是可能 android.jar 包中的某个类 作为引证,引证到 了用 dex2jar 生成的jar 包中的类)

(3) 递归一切引证的写法:主要思想便是 DFS. 这儿写出来的 maindexlist 有一些是系统类 可是没有关系,只要能悉数覆盖smali 中的类就行。

/**
 * 找出 className 类 ,递归该类一切引证并增加到 mainDexList  DFS 算法
 * @param className 类名
 * @param pool
 */
   public static void getRefClass(String className, ClassPool pool,HashSet<String> mainDexClassList) {
   if (className == null) return;     // 不需求遍历
   if (className.contains("java.") || className.contains("android.os.")) return;
   //  jdk 和 Android 一些系统类,不需求遍历 
   CtClass cc = null;
   try {
       cc = pool.get(className);   // 取得 CtClass 实例
   } catch (NotFoundException e) {
       return;
   }
   mainDexClassList.add(className);   // 将该 类增加进 maindexclasslist ,比较现已访问
   for (String name : cc.getRefClasses()) {  // 获取该类的引证类
       if (!name.contains("java.")) {
           if (!mainDexClassList.contains(name)) {  // 假如引证类中还有没访问过的,递归调用
               mainDexClassList.add(name);
               getRefClass(name, pool,mainDexClassList);
           }
       }
   }
 }

(4) 获取 subClassList 中一切子类的直接引证类和依靠引证类放进 maindexlist 类中

/**
 * 判别 currentClass 是否是 subClassList 列表中的类 的子类
 * 是的话 将 currentClass 的直接引证参加 mainDexList,并将 currentClass 参加 subClassList
 * @param pool classPool 对象
 * @param mainDexList mainDexList 一切应该放在主 dex 的类
 * @param currentClass 当时类
 * @param subClassList
 * @return
 * @throws NotFoundException
 */
 private static boolean isHaveParentClass(ClassPool pool, Set<String> mainDexList, String currentClass, Set<String> subClassList) throws NotFoundException {
   if (subClassList.contains(currentClass)) { // currentClass现已包含  currentClass 
       return true;
   }
   CtClass ctClass = null;
   CtClass superClass = null;
   try {
       ctClass = pool.getCtClass(currentClass);
       superClass = ctClass.getSuperclass(); // 取得 currentClass 的父类
   } catch (NotFoundException e) {
       return false;
   }
   if (superClass != null) {
       if (isHaveParentClass(pool, mainDexList, superClass.getName(), subClassList)) {// 递归判别父类是否是 currentClass 中的类,是的话 将 currentClass 加上,并递归引证类
           subClassList.add(currentClass);
           getRefClass(currentClass,pool,mainDexList);
           return true;
       }
   }
   return false;
 }

(5) 对 maindexlist中的类判别是否有匿名内部类,有的话也加上。

/**
* 获取 mainDexClassList 中一切类的匿名内部类 并增加到 resultList, 同时将 mainDexClassList 增加到 resultList
*/
   public static void getNestedClassList(HashSet<String> mainDexClassList ,HashSet<String> resultList) {
   ArrayList<String> smaliListPathList = SmaliPreProcessHelper.getSmaliListPath(SmaliDataPath);
   resultList.addAll(mainDexClassList);
   for (String name : mainDexClassList) {
       String lastName = name.substring(name.lastIndexOf(".") + 1);
       String namePath = name.replaceAll("\\.", quoteReplacement(File.separator));
       for (String path : smaliListPathList) {
           File file = new File(path + File.separator + namePath + ".smali");
           if (file.exists()) {
               File parentFile = file.getParentFile();
               for (File f : parentFile.listFiles()) {
                   if (f.getName().startsWith(lastName + "$")) { // 判别当时该类是否有匿名内部类
                       String nestClassName = name.substring(0, name.lastIndexOf(".") + 1) + f.getName().replace(".smali", "");
                       resultList.add(nestClassName);
                   }
               }
           }
       }
   }
 }

(6) 依据 maindexlist 中的类从smali 文件夹中进行移动并作为主 smali, 其他 smali 文件作为其他 smali

/**
 *  依据 mainDexList 从原先的一切smali 文件夹中 移动到 主 smali ,其余作为 副 smali
 *  @param smaliPathList  原先一切 smali 文件夹途径
 *  @param mainDexList  放在主 dex 的 类 的 list
 *  @param parentOriginalPath 移动到的新文件夹途径
 */
    public static void moveMainSmali(ArrayList<String> smaliPathList, String[] mainDexList, String parentOriginalPath) {
  // 作为主 smali
    String smaliMainFilePath = parentOriginalPath + File.separator + "smali";
    for (String name : mainDexList) {
        name = name.replaceAll("\\.", quoteReplacement(File.separator));
        for (String parentPath : smaliPathList) {
            String smaliFilePath = parentPath + File.separator + name + ".smali";
            File file = new File(smaliFilePath);
            if (file.exists()) {
                File targetFile = new File(smaliMainFilePath + File.separator + name + ".smali");
                FileUtil.checkParentFile(smaliMainFilePath + File.separator + name + ".smali");//判别targetFile 是否有父目录,没有的话创建
                file.renameTo(targetFile);//  依据 mainDexList 中的目录进行移动
                break;
            }
        }
    }
// 其他 smali 文件夹 命名为 smali_classesxx 并进行复制到 跟主 smali 同一个父目录下
    for (int i = smaliPathList.size() + 1; i >= 2; i--) {
        String path = smaliPathList.get(i - 2);
        String newFilePath = parentOriginalPath + File.separator + "smali_classes" + i;
        LogUtil.d(path);
        LogUtil.d(path + " 复制到:" + parentOriginalPath + File.separator + "smali_classes" + i);
        FileUtil.copyDirectiory(path, newFilePath,false);
    }

(7) 判别 multidex 运用 ,判别当时 smali 是否有运用 multidex,没有的话自己加上。 关于multidex 的支撑库而言,能够找一个依靠了 multidex 的 apk 反编译后,找到 smali/android/support/multidex ,这个目录下便是 multidex 支撑库下的 smali 代码,将这份代码复制到自己的 smali 对应目录下就能够了。

// 检测是否现已运用了 multidex
private static boolean checkIsUseMultiDex(String path, String AndroidManifestFilePath, ClassPool classPool) throws DocumentException, NotFoundException {
    //1 判别是否有承继 MultiDexApplication 
    ManifestProcessor manifestProcessor = new ManifestProcessor(AndroidManifestFilePath);
    String ApplicationName = manifestProcessor.getAppProcessor().getApplicationName();
    CtClass ctClass = classPool.getCtClass(ApplicationName);
    //
    while (!ctClass.getName().equals("android.app.Application")) {
        System.out.println(ctClass.getName());
        ctClass = ctClass.getSuperclass();
        if (ctClass.getName().equals("android.support.multidex.MultiDexApplication")) {
            System.out.println("存在承继 MultiDexApplication 的 Application 类");
            return true;
        }
    }
// 2 是否有调用  MultiDex.install() 
String namePath = path + File.separator + ApplicationName.replaceAll("\\.", quoteReplacement(File.separator)) + ".smali";
System.out.println(namePath);
File applicationFile = new File(namePath);
if (applicationFile.exists()) {
    String content = FileUtil.read(namePath);
    System.out.println(content);
    // MultiDex.install() --》smali 写法
    if (content.contains("Landroid/support/multidex/MultiDex;->install(Landroid/content/Context;)V")) {
        System.out.println("主 Application 调用了 multidex.install() 办法");
        return true;
    }
}
return false;
}

(8) 回编译并 查看低版别手机运转状况 注:有时分关于经过反射来实例化的类没办法知道,所以只能自己手动加上去。

一些运转的日志截图: 办法数超 65535 打包失利进行主动分多 dex:

安卓游戏发行-打包 65535 方法数超?来个自动分多 dex

从头打包成功:

安卓游戏发行-打包 65535 方法数超?来个自动分多 dex