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

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
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值