前言:


当一个App发布之后,突然发现了一个严重bug需要进行紧急修复,这时候公司各方就会忙得焦头烂额:重新打包App、测试、向各个应用市场和渠道换包、提示用户升级、用户下载、覆盖安装。有时候仅仅是为了修改了一行代码,也要付出巨大的成本进行换包和重新发布。


这时候就提出一个问题:有没有办法以补丁的方式动态修复紧急Bug,不再需要重新发布App,不再需要用户重新下载,覆盖安装?


虽然Android系统并没有提供这个技术,但是很幸运的告诉大家,答案是:可以,Android可以使用热补丁动态修复技术来解决以上这些问题。




解决方案




简单的概括一下,就是把多个dex文件塞入到app的classloader之中,但是androiddex拆包方案中的类是没有重复的,如果classes.dex和classes1.dex中有重复的类,当用到这个重复的类的时候,系统会选择哪个类进行加载呢?





ClassLoader机制






一个ClassLoader可以包含多个dex文件,每个dex文件是一个Element,多个dex文件排列成一个有序的数组dexElements,当找类的时候,会按顺序遍历dex文件,然后从当前遍历的dex文件中找类,如果找类则返回,如果找不到从下一个dex文件继续查找。



理论上,如果在不同的dex中有相同的类存在,那么会优先选择排在前面的dex文件的类,如下图:




Android 系统补丁查看 android 补丁更新_补丁



技术实现:




创建DexLoaderUtil 类,默认加载Asset中包含定义控件BeatView的Dex包,如果Sdcard 根目录下有Dex,则优先加载sdcard

public class DexLoaderUtil { 
   
     private static final String TAG = "DexLoaderUtil";    
     public static final String HOTLIB3_DEX_NAME = "HotLib3.dex";    
     public static final String HOTLIB3_CLASS_NAME = "com.example.hotlib.BeatView";    
     //public static final String HOTLIB3_CLASS_NAME_FIX = "com.example.hotlib.BugClass";    
     private static final int BUF_SIZE = 8 * 1024;    
     private static final String DEX_PATH = "sdcard/";    


     public static String getDexPath(Context context, String dexName) {    
         return new File(context.getDir("dex", Context.MODE_PRIVATE), dexName)    
                 .getAbsolutePath();    
     }    


     public static String getOptimizedDexPath(Context context) {    
         return context.getDir("outdex", Context.MODE_PRIVATE).getAbsolutePath();    
     }    


     public static boolean copysdCardDex(Context context, String dexName) {    
         boolean bRes = false;    
         File dexInternalStoragePath = new File(context.getDir("dex",    
                 Context.MODE_PRIVATE), dexName);    
         BufferedInputStream bis = null;    
         OutputStream dexWriter = null;    


         try {    
             File file = new File(DEX_PATH + dexName);    
             bis = new BufferedInputStream(new FileInputStream(file));    
             // bis = new BufferedInputStream(context.getAssets().open(dexName));    
             dexWriter = new BufferedOutputStream(new FileOutputStream(    
                     dexInternalStoragePath));    
             byte[] buf = new byte[BUF_SIZE];    
             int len;    
             while ((len = bis.read(buf, 0, BUF_SIZE)) > 0) {    
                 dexWriter.write(buf, 0, len);    
             }    
             dexWriter.close();    
             bis.close();    
             bRes = true;    


         } catch (FileNotFoundException e) {    
             Log.d("Qinghua", e.getMessage());    
             e.printStackTrace();    
         } catch (IOException e) {    
             Log.d("Qinghua", e.getMessage());    
             e.printStackTrace();    
         }    
         Log.d("Qinghua", DEX_PATH + dexName + " " + bRes);    
         return bRes;    
     }    


     public static void copyDex(Context context, String dexName) {    
         if (!copysdCardDex(context, dexName)) {    
             File dexInternalStoragePath = new File(context.getDir("dex",    
                     Context.MODE_PRIVATE), dexName);    
             BufferedInputStream bis = null;    
             OutputStream dexWriter = null;    


             try {    
                 // File file = new File(DEX_PATH+dexName);    
                 // bis = new BufferedInputStream(new FileInputStream(file));    
                 bis = new BufferedInputStream(context.getAssets().open(dexName));    
                 dexWriter = new BufferedOutputStream(new FileOutputStream(    
                         dexInternalStoragePath));    
                 byte[] buf = new byte[BUF_SIZE];    
                 int len;    
                 while ((len = bis.read(buf, 0, BUF_SIZE)) > 0) {    
                     dexWriter.write(buf, 0, len);    
                 }    
                 dexWriter.close();    
                 bis.close();    


             } catch (FileNotFoundException e) {    
                 Log.d("Qinghua", e.getMessage());    
                 e.printStackTrace();    
             } catch (IOException e) {    
                 Log.d("Qinghua", e.getMessage());    
                 e.printStackTrace();    
             }    


         }    
     }    


     public static void loadAndCall(Context context, String dexName,    
             String className) {    
         final File dexInternalStoragePath = new File(context.getDir("dex",    
                 Context.MODE_PRIVATE), dexName);    
         final File optimizedDexOutputPath = context.getDir("outdex",    
                 Context.MODE_PRIVATE);    


         DexClassLoader cl = new DexClassLoader(    
                 dexInternalStoragePath.getAbsolutePath(),    
                 optimizedDexOutputPath.getAbsolutePath(), null,    
                 context.getClassLoader());    
         call(cl, className);    
     }    


     public static void call(ClassLoader cl, String className, Object instance,    
             String functionName) {    
         // String str = "";    
         Class myClasz = null;    
         try {    
             myClasz = cl.loadClass(className);    
             myClasz.getDeclaredMethod(functionName).invoke(instance);    
             // .toString();    
         } catch (ClassNotFoundException e) {    
             e.printStackTrace();    
         } catch (InvocationTargetException e) {    
             e.printStackTrace();    
         } catch (NoSuchMethodException e) {    
             e.printStackTrace();    
         } catch (IllegalAccessException e) {    
             e.printStackTrace();    
         }    
         // return str;    
     }    


     public static Object callInstance(ClassLoader cl, String className,    
             Context para) {    
         Object str = null;    
         Class myClasz = null;    
         try {    
             myClasz = cl.loadClass(className);    
             // Object instance    
             str = myClasz.getConstructor(Context.class).newInstance(para);    
                 
         } catch (ClassNotFoundException e) {    
             e.printStackTrace();    
         } catch (InvocationTargetException e) {    
             e.printStackTrace();    
         } catch (NoSuchMethodException e) {    
             e.printStackTrace();    
         } catch (IllegalAccessException e) {    
             e.printStackTrace();    
         } catch (InstantiationException e) {    
             e.printStackTrace();    
         }    
         return str;    
     }    


     public static String call(ClassLoader cl, String className) {    
         String str = "";    
         Class myClasz = null;    
         try {    
             myClasz = cl.loadClass(className);    
             Object instance = myClasz.getConstructor().newInstance();    
             // str = myClasz.getDeclaredMethod("test1",    
             // String.class).invoke(instance, "cissy").toString();    
             str = myClasz.getDeclaredMethod("test1").invoke(instance)    
                     .toString();    
         } catch (ClassNotFoundException e) {    
             e.printStackTrace();    
         } catch (InvocationTargetException e) {    
             e.printStackTrace();    
         } catch (NoSuchMethodException e) {    
             e.printStackTrace();    
         } catch (IllegalAccessException e) {    
             e.printStackTrace();    
         } catch (InstantiationException e) {    
             e.printStackTrace();    
         }    
         return str;    
     }    


     public static synchronized Boolean injectAboveEqualApiLevel14(    
             String dexPath, String defaultDexOptPath, String nativeLibPath,    
             String dummyClassName) {    
         Log.i(TAG, "--> injectAboveEqualApiLevel14");    
         PathClassLoader pathClassLoader = (PathClassLoader) DexLoaderUtil.class    
                 .getClassLoader();    
         DexClassLoader dexClassLoader = new DexClassLoader(dexPath,    
                 defaultDexOptPath, nativeLibPath, pathClassLoader);    
         try {    
             dexClassLoader.loadClass(dummyClassName);    
             Object dexElements = combineArray(    
                     getDexElements(getPathList(pathClassLoader)),    
                     getDexElements(getPathList(dexClassLoader)));    


             Object pathList = getPathList(pathClassLoader);    
             setField(pathList, pathList.getClass(), "dexElements", dexElements);    
         } catch (Throwable e) {    
             e.printStackTrace();    
             return false;    
         }    
         Log.i(TAG, "<-- injectAboveEqualApiLevel14 End.");    
         return true;    
     }    


     private static Object getPathList(Object baseDexClassLoader)    
             throws IllegalArgumentException, NoSuchFieldException,    
             IllegalAccessException, ClassNotFoundException {    
         return getField(baseDexClassLoader,    
                 Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");    
     }    


     private static Object getDexElements(Object paramObject)    
             throws IllegalArgumentException, NoSuchFieldException,    
             IllegalAccessException {    
         return getField(paramObject, paramObject.getClass(), "dexElements");    
     }    


     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 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;    
     }    
 }

Activity Class 调用:


public class MainActivity extends ActionBarActivity {
     private Button dowork3;
     Context ctx;
     RelativeLayout rl;


     private updateDatabaseReceiver mUpdateDatabaseReceiver;


     @Override
     protected void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
         setContentView(R.layout.activity_main);
         ctx = this;
         new innitDexThread().start();
         dowork3 = (Button) findViewById(R.id.work3);
         dowork3.setText("beatview start");
         dowork3.setOnClickListener(new OnClickListener() {


             @Override
             public void onClick(View arg0) {
                 DexLoaderUtil.call(getClassLoader(),
                         DexLoaderUtil.HOTLIB3_CLASS_NAME, rl.getChildAt(0),
                         "start");
             }


         });
         rl = (RelativeLayout) findViewById(R.id.beatviewP);
     }


     Handler loadHnadler = new Handler() {


         @Override
         public void handleMessage(Message msg) {
             switch (msg.what) {
             case 0: {
                 Toast to = Toast.makeText(getApplicationContext(), String
                         .format("Init dex file:%s done!",
                                 DexLoaderUtil.HOTLIB3_DEX_NAME),
                         Toast.LENGTH_SHORT);
                 to.show();
                 new accSouThreadBeatInit(ctx).start();
                 break;
             }
             case 3: {
                 Toast to = Toast.makeText(getApplicationContext(),
                         "init beatview done!", Toast.LENGTH_SHORT);
                 to.show();
                 rl.addView((View) msg.obj);
                 break;
             }
             default:


             }
         }
     };


     public class accSouThreadBeatInit extends Thread {
         Context mctx;


         public accSouThreadBeatInit(Context mctxP) {
             this.mctx = mctxP;
         }


         @Override
         public void run() {
             super.run();
             Object obj = DexLoaderUtil.callInstance(getClassLoader(),
                     DexLoaderUtil.HOTLIB3_CLASS_NAME, mctx);


             Message msg = loadHnadler.obtainMessage();


             if (null == obj) {
                 Log.e("Qinghua", "xxxxxxxxxxxxxxxxxxx");
             }
             msg.what = 3;
             msg.obj = obj;


             // msg.what = P.SEARCHCOMPLETE;
             loadHnadler.sendMessageDelayed(msg, 20);
         }
     }


     public class innitDexThread extends Thread {
         @Override
         public void run() {
             super.run();


             if (!GerSharePerferencesInfo.getDexUpdated(getApplicationContext())) {
                 DexLoaderUtil.copyDex(MainActivity.this,
                         DexLoaderUtil.HOTLIB3_DEX_NAME);


                 GerSharePerferencesInfo.setDexUpdated(getApplicationContext(),
                         true);
             }
             
             String HotLib3DexPath = DexLoaderUtil.getDexPath(MainActivity.this,
                     DexLoaderUtil.HOTLIB3_DEX_NAME);
             
             DexLoaderUtil.injectAboveEqualApiLevel14(HotLib3DexPath,
                     optimizedDexOutputPath, null,
                     DexLoaderUtil.HOTLIB3_CLASS_NAME);
             Message msg = loadHnadler.obtainMessage();
             msg.what = 0;
             loadHnadler.sendMessageDelayed(msg, 20);
         }
     }


 }




效果:


Android 系统补丁查看 android 补丁更新_热更新_02




补丁更新




现在考虑到BeatView显示每秒帧数高,CPU占用率大,修改BeatView Code修复这个bug,重新包Dex,并放在Sdcard要目录下,去做热补丁更新:




Android 系统补丁查看 android 补丁更新_发布_03


重新启动APP的效果:




Android 系统补丁查看 android 补丁更新_Dex 分包_04




结果:




BeatView帧数降低了,整个过程中没有重新安装APP 或做Update,在用户简单的重启APP过程中就修复了Bug。




结语:




现实项目中做Dex补丁更新,还需要了解相关的分包方案,网络推送,版本控制,参数设定(Google Tag Manager)等具体细节,每一个展开说都很有内涵,本文不再细述。