Android Dex 分包+热修复(QQ空间技术方案)

论坛 期权论坛 脚本     
已经匿名di用户   2022-7-2 21:48   2579   0
Android Dex 分包+热修复(QQ空间技术方案)
感谢博主的博客:
主要代码:上面博主的github: MultiDex Demo
如果用的是AS,可以参考 multidex-sampleAndroid的multidex带来的性能问题-减慢app启动速度;里面有个检测APP引用了哪些其他dex文件中的类的工具类,便于main-dex-rule.xml的收集,避免出NoClassDefFoundError以及优化启动速度。
博主将主要类打成classes.dex,其他业务类打成classes2.dex,将jar包打成classes3.dex,这样分工明细,出错也只会出在业务类(一般jar不会出错,主类也一般是不能有问题的),线上修复的时候只需替换classes2.dex即可。但美中不足之处在于,没有将本项目和依赖项目中的so文件进行拷贝,那么完善的build.xml如下(如有误,还请各位指教):


    

       

     

     

     

     

     

     

     

      
       1 初始化,删除bin和gen目录
      

      

      

      

      

      

     

     


     

     


     

     

     

     

     

     

     

     

     

     

     

     

      
       
2.1 生成工程的R.java 文件,输出到 gen目录,此时需要把依赖工程的res资源一起生成R.java

      

      
       
aapt package -m -J ${project-dir}/gen -M ${manifest} -S ${project-dir}/res -S ${baidu-dir}/res -I ${android-jar}

      

      

       

       

       

       

       

       

       

       

       

       

       

       

       

      

      

      
       2.2 生成依赖工程的R.java 文件,输出到 gen目录
      

      
       
aapt package -m -J ${project-dir}/gen -M ${baidu.manifest} -S ${project-dir}/res -S ${baidu-dir}/res -I ${android-jar}

      

      

       

       

       

       

       

       

       

       

       

       

       

       

       

      

     


     

     

       
            
       
            
       
            
       
            
       
            
       
            
       
            
       
                
        
            
       
          
      
-->  


     

      
       3.1 编译依赖工程的Java文件,输出到 bin/classes目录
      

      

        
       

       

       

       

       

        

        

       

      

      
       3.2 编译项目工程的Java文件,输出到 bin/classes目录
      

      
       
javac -bootclasspath ${android-jar} -d ${project-dir}/bin/classes ${project-dir}/src gen/R.java

      

      

       

       

       

        

        

       

       

        

        

       

      

     

     

     

     

      
       
4.1 构建dex主包和次包;分为三个部分 主包dex包含定义的文件,剩下的在classes2.dex 所有的jar都在classes3.dex

      

      
       
dx --dex --multi-dex --set-max-idx-number=20000 --main-dex-list ${project-dir}/main-dex-rule.txt --minimal-main-dex --output=${project-dir}/bin

      

      

       

       

       

       

       

       

       

       

      

      
       4.2 构建项目和依赖项目所包含的jar
      

      
       
de --dex --output=bin/classes3.dex ${project-dir}/libs ${baidu-dir}/libs

      

      

       

       

       

       

      

     

     

      
       
5 将res和assets,AndroidManifest.xml 打包为resources.arsc

      

      
       
如果依赖项目中使用了自己项目下的aeests目录下资源,需要在 生成 R 文件,以及 打包时一并带上,这里没写

      

      
       
aapt package -f -M ${manifest} -S res -S ${baidu-dir}/res -A assets -I ${android-jar} -F ${project-dir}/bin/resources.arsc --auto-add-overlay

      

      

       

       

       

       

       

       

       

       

       

       

       

       

       

       

       

       

       

       

       

      

     

     

      
       6 将 classes.dex文件和resources.arsc打包成临时APK
      

      
       
注: 1,如果需要将so文件打包进apk,一定要加上-nf参数 2,如果第三方jar包里含有图片资源,一定要加上-rj参数,不然jar包里资源文件解不出来,程序会因为无法引用资源而报错!

      

      
       
java ${sdk-folder}/tools/lib/sdklib.jar/com.android.sdklib.build.ApkBuilderMain ${project-dir}/bin/unsign.apk -u -z ${project-dir}/bin/resources.arsc -f bin/classes.dex -rf ${project-dir}/src -rf ${baidu-dir}/src -rj ${project-dir}/libs -rj ${baidu-dir}/libs -nf ${project-dir}/libs -nf ${baidu-dir}/libs

      

      

       

       

       

       

       

       

       

       

       

       

       

       

       

       

       

       

       

       

       

       

       

       

       

       

       

       

       

      

     

     

     

     

     

     

      
       
7 复制所有bin/classes*.dex文件到项目根目录,因为我们的脚本是在根目录下面,这样在运行aapt的时候,可以直接操作dex文件了

      

      

       

        

       

      

     

     

      
       8 循环将bin/classes*.dex文件 aapt 添加到apk中"
      

      

       

        

       

      

     

     

      

      

      

      

       

       

        
         ${dexfile} 已经打包进了apk,这里不在添加
        

       

       

        
         ${dexfile} 需要添加进apk
        

        
         aapt add bin/unsign.apk ${dexfile}
        

         
          
          
          
        

       

      

      
       添加完成,将项目根目录下的 ${dexfile} 删除
      

      

     

     

      
       9 生成签名的apk
      

      
       
jarsigner -keystore ${project-dir}/my.keystore -storepass 123456 -keypass 123456 -signedjar ${project-dir}/bin/sign.apk ${project-dir}/bin/unsign.apk ant_test

      

      

       

       

       

       

       

       

       

       

       

       

      

     

     

     

      
       10 删除bin/resources.arsc和bin/unsign.apk; 对APK进行对齐优化
      

      
       
zipalign 4 ${project-dir}/bin/sign.apk ${project-dir}/bin/${ant.project.name}_signed_zipaligned.apk

      

      

       

       

       

      

      

      

      

     

    com/alex_mahao/multidex/MainActivity.class
com/alex_mahao/multidex/MyApp.class
com/alex_mahao/multidex/FixDexUtils.class
com/alex_mahao/multidex/FileUtils.class

**分包注意:
1.Android 5.0 以上的系统,默认会加载多个dex,5.0以下的版本需要手动加载dex(在Application中重写attachBaseContent(Context base) 方法,调用 SecondaryDexUtils.loadSecondaryDex(base));
2.先配置Ant编译环境(注意下ant-contrib-1.0b3.jar 放到ant的lib目录下面
3.将SDK plant-tools 更新到20.0.0以上(20.0.0以下的dx.bat 不支持--multidex);
4.必须将Application中引用到的直接类写在main-dex-rule.xml里面,如果引用的类种有内部类,那么也要写上,如:
com/huyu/MainActivity.class
com/huyu/MainActivity$Loaddex.class (Loaddex 是 MainActivity 中的内部类)
**动态加载 dex :
1.网上很多原理分析以及demo,但是在5.0以下的手机上还是会有问题。
我将网上的 SecondaryDexUtils 改良后如下:

主要更改的部分逻辑:
1.在APP每次启动时,判断dex存放目录: data/data/<packageName>/app_odex/ 下是否存在从APK解压出来的dex文件,或者是从服务器上下载下来的需要修复的dex文件(这里才是热修复的地方);
2.如果不存在,就是APP安装后初次启动,需要从APK里解压出来(只解压除了classes.dex之外的dex)。
3.如果存在,就直接执行注入;
4.热修复的时候,只需从服务器上下载dex文件,先删除data/data/<packageName>/app_odex/ 目录下的要替换的dex文件,再将dex文件拷贝到data/data/<packageName>/app_odex/ 目录下即可
dex存放目录:可以通过 context.getDir("odex",Context.MODE_PRIVATE).getAbsolutePath();获得;如果该目录不存在,系统会自动创建,并在 “odex”前加上“app_”的标识即 “app_odex”。

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import android.content.Context;
import android.os.Build;
import android.util.Log;
import dalvik.system.DexClassLoader;
import dalvik.system.PathClassLoader;

public class SecondaryDexUtils {

    public static final boolean ON = true;

    //use classes2.dex.lzma in apk from android 4.x+
    //use classes2.dex in apk for android 5.x+ 4.x-

    private static final String TAG = "TAG_注入Dex";

    /***************************************/
    private static int SUB_DEX_NUM = 10;
    private static final String CLASSES_PREFIX      = "classes";
    private static final String DEX_POSTFIX         = ".dex";
    private static final HashSet
    
      msLoadedDexList = new HashSet
     
      ();
    /***************************************/

    private static final int BUF_SIZE = 1024 * 32;
    private static String mSubdexExt = DEX_POSTFIX;

    private static class LoadedDex{
        private File   dexFile;
        private ZipEntry zipEntry;
        private LoadedDex(File dir,String name){
            dexFile = new File(dir,name);
        }
        private LoadedDex(File dir,String name,ZipEntry zipE){
            dexFile = new File(dir,name);
            zipEntry = zipE;
        }
    }
    static{
        msLoadedDexList.clear();
    }

/*
    public static final File getCodeCacheDir(Context context) {
     ApplicationInfo appInfo = context.getApplicationInfo();
     return createFilesDir(new File(appInfo.dataDir, "dex_cache"));
    }
*/
/*
    private synchronized static File createFilesDir(File file) {
        if (!file.exists()) {
            if (!file.mkdirs()) {
                if (file.exists()) {
                    return file;
                }
                Log.e(TAG, "创建文件夹失败:" + file.getPath());
                return null;
            }
        }
        return file;
    }
*/

    /**
     * 复制子dex
     * @param inputStream
     * @param outputFile
     * @return
     */
    public static boolean copydexFile(InputStream inputStream,File outputFile) {

        BufferedInputStream bis = null;
        OutputStream dexWriter = null;

        try {
            bis = new BufferedInputStream(inputStream);
            assert bis != null;

            dexWriter = new BufferedOutputStream(new FileOutputStream(outputFile));
            byte[] buf = new byte[BUF_SIZE];
            int len;
            while ((len = bis.read(buf, 0, BUF_SIZE)) > 0) {
                dexWriter.write(buf, 0, len);
            }

        } catch (IOException e) {
            return false;
        } finally {
            if (null != dexWriter)
                try {
                    dexWriter.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }

            if (bis != null)
                try {
                    bis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
        }
        return true;
    }

    /**
     * 加载子dex
     * @param appContext
     */
 public static void loadSecondaryDex(Context appContext) {
     
        if(appContext == null){
            return;
        }

        ZipFile apkFile = null;
        try {
            apkFile = new ZipFile(appContext.getApplicationInfo().sourceDir);
        } catch (Exception e) {
         Log.i(TAG, "create zipfile error:"+Log.getStackTraceString(e));
            return;
        }
        
        Log.i(TAG, "APK-zipfile:"+apkFile.getName());
        File filesDir = appContext.getDir("odex", Context.MODE_PRIVATE);
        Log.i(TAG, "APK-复制子dex的目标路径:"+filesDir.getAbsolutePath());
        
        for(int i = 0 ; i < SUB_DEX_NUM; i ++){
            String possibleDexName = buildDexFullName(i);
            ZipEntry zipEntry = apkFile.getEntry(possibleDexName);
            Log.i(TAG, "APK下的entry:"+zipEntry);
            if(zipEntry == null) {
                break;
            }
            msLoadedDexList.add(new LoadedDex(filesDir,possibleDexName,zipEntry));
        }
        Log.i(TAG, "子dex总数:"+msLoadedDexList.size());
        
        //判断  目标目录下是否已经有dex文件
        boolean isOpted = false;
        File[] listFiles = filesDir.listFiles();
        for (int i = 0; i < listFiles.length; i++) {
         File file = listFiles[i];
         if(file.isFile() && file.getName().endsWith(".dex")){
          isOpted = true;
          break;
         }
  }
        
        //  data/data/
      
       /app_odex 目录下存在.dex文件 就不再从APK解压,否则从APK解压
        if(!isOpted){
         for (LoadedDex loadedDex : msLoadedDexList) {
          File dexFile = loadedDex.dexFile;
          try {
           boolean result = copydexFile(apkFile.getInputStream(loadedDex.zipEntry), dexFile);
           Log.i(TAG, "复制子dex结果:"+result);
          } catch (Exception e) {
           Log.i(TAG, "复制子dex错误:"+Log.getStackTraceString(e));
          }
         }

         if (apkFile != null) {
          try {
           apkFile.close();
          } catch (Exception e) {
          }
         }
        }
        doDexInject(appContext, filesDir, msLoadedDexList);
    }

    private static String buildDexFullName(int index){
        return CLASSES_PREFIX + (index + 2) + mSubdexExt;
    }
    
    private static void doDexInject(final Context appContext, File filesDir,HashSet
       
         loadedDex) {
        if(Build.VERSION.SDK_INT >= 23){
            Log.w(TAG,"无法注入dex,SDK版本太高;版本=" + Build.VERSION.SDK_INT);
        }
        String optimizeDir = filesDir.getAbsolutePath() + File.separator + "opt_dex";
        File fopt = new File(optimizeDir);
        if (fopt.exists())
         fopt.delete();
        fopt.mkdirs();

        try {
            ArrayList
        
          dexFiles = new ArrayList
         
          (); for(LoadedDex dex : loadedDex){ dexFiles.add(dex.dexFile); DexClassLoader classLoader = new DexClassLoader( dex.dexFile.getAbsolutePath(), fopt.getAbsolutePath(),null, appContext.getClassLoader()); inject(classLoader, appContext); } } catch (Exception e) { Log.i(TAG, "install dex error:"+Log.getStackTraceString(e)); } } /** * @param loader */ private static void inject(DexClassLoader loader, Context ctx){ PathClassLoader pathLoader = (PathClassLoader) ctx.getClassLoader(); try { Object dexElements = combineArray( getDexElements(getPathList(pathLoader)), getDexElements(getPathList(loader))); Object pathList = getPathList(pathLoader); setField(pathList, pathList.getClass(), "dexElements", dexElements); } catch (Exception e) { Log.i(TAG, "inject dexclassloader error:" + Log.getStackTraceString(e)); } } private static Object getPathList(Object baseDexClassLoader) throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException { return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList"); } private static Object getField(Object obj, Class
           cl, String field) throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException { Field localField = cl.getDeclaredField(field); localField.setAccessible(true); return localField.get(obj); } private static Object getDexElements(Object paramObject) throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException { return getField(paramObject, paramObject.getClass(), "dexElements"); } private static void setField(Object obj, Class
           cl, String field, Object value) throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException { Field localField = cl.getDeclaredField(field); localField.setAccessible(true); localField.set(obj, value); } private static Object combineArray(Object arrayLhs, Object arrayRhs) { Class
           localClass = arrayLhs.getClass().getComponentType(); int i = Array.getLength(arrayLhs); int j = i + Array.getLength(arrayRhs); Object result = Array.newInstance(localClass, j); for (int k = 0; k < j; ++k) { if (k < i) { Array.set(result, k, Array.get(arrayLhs, k)); } else { Array.set(result, k, Array.get(arrayRhs, k - i)); } } return result; } /**删除文件*/ public static boolean deleteFile(String path){ File file = new File(path); if(file.exists()) return file.delete(); return true; } }
         
        
       
      
     
    
/**这里省略了从服务器下载dex的代码,直接将dex放在了SD卡根目录,直接替换dex;如果文件比较大最好开启线程去复制*/
public void inject(View view) {
 // 无bug的classes2.dex 存放 到SD卡 根目录
 String sourceFile = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "classes2.dex";
 // 系统的私有目录
 String targetFile = getDir("odex", Context.MODE_PRIVATE).getAbsolutePath() + File.separator + "classes2.dex";
 try {
  //先把  data/data/
     
      /app_odex/classes2.dex   删除
  SecondaryDexUtils.deleteFile(targetFile);
  // 复制文件到私有目录
  SecondaryDexUtils.copydexFile(new FileInputStream(sourceFile), new File(targetFile));
  // 删除 SD卡上的 classes2.dex
  SecondaryDexUtils.deleteFile(sourceFile);
 } catch (Exception e) {
  e.printStackTrace();
 }
}
     



最后关于性能方面:

在冷启动时因为需要加载多个DEX文件,如果DEX文件过大时,处理时间过长,很容易引发ANR(Application Not Responding);采用MultiDex方案的应用可能不能在低于Android 4.0 (API level 14) 机器上启动,这个主要是因为Dalvik linearAlloc的一个bug (Issue 22586);采用MultiDex方案的应用因为需要申请一个很大的内存,在运行时可能导致程序的崩溃,这个主要是因为Dalvik linearAlloc 的一个限制(Issue 78035). 这个限制在 Android 4.0 (API level 14)已经增加了, 应用也有可能在低于 Android 5.0 (API level 21)版本的机器上触发这个限制。

Dex分包后,如果是启动时同步加载,对应用的启动速度会有一定的影响(主要表现为白屏或黑屏,这个好像与Theme有关),但是主要影响的是安装后首次启动。这是因为安装后首次启动时,Android系统会对加载的从dex做Dexopt并生成ODEX,而 Dexopt 是比较耗时的操作,所以对安装后首次启动速度影响较大。在非安装后首次启动时,应用只需加载 ODEX,这个过程速度很快,对启动速度影响不大。同时,从dex 的大小也直接影响启动速度,即从dex越小则启动越快。

后面将继续优化,可以考虑APP初次启动时打开启动图,在这段时间内加载dex,验证后将验证结果贴上来。

分享到 :
0 人收藏
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

积分:81
帖子:4969
精华:0
期权论坛 期权论坛
发布
内容

下载期权论坛手机APP