Android熱更新方案之阿里AndFix 原理以及原始碼解析

Android入墳之路發表於2019-03-28

原理:

Android熱更新方案之阿里AndFix 原理以及原始碼解析

修復過程:

Android熱更新方案之阿里AndFix 原理以及原始碼解析

原始碼解析過程如下:

patchManager=newPatchManager(context);

patchManager.init(appversion);//current version

patchManager做了以下工作:

<pre style="margin: 0px; padding: 0px; font-size: 0.85em; font-family: monospace, serif; overflow: auto; line-height: 1.45; background-color: rgb(248, 248, 248); border-radius: 0.5em; position: relative; border: 1px solid rgb(238, 238, 238);">public PatchManager(Context context) {
   mContext = context;
   mAndFixManager = new AndFixManager(mContext);
   mPatchDir = new File(mContext.getFilesDir(), *DIR*);
   mPatchs = new ConcurrentSkipListSet<Patch>();
   mLoaders = new ConcurrentHashMap<String, ClassLoader>();
}</pre>
複製程式碼

new了個AndFixmanager,看一下

<pre style="margin: 0px; padding: 0px; font-size: 0.85em; font-family: monospace, serif; overflow: auto; line-height: 1.45; background-color: rgb(248, 248, 248); border-radius: 0.5em; position: relative; border: 1px solid rgb(238, 238, 238);">public AndFixManager(Context context) {
   mContext = context;
   mSupport = Compat.*isSupport*();
   if (mSupport) {
      mSecurityChecker = new SecurityChecker(mContext);
      mOptDir = new File(mContext.getFilesDir(), *DIR*);
      if (!mOptDir.exists() && !mOptDir.mkdirs()) {// make directory fail
         mSupport = false;
         Log.*e*(*TAG*, "opt dir create error.");
      } else if (!mOptDir.isDirectory()) {// not directory
         mOptDir.delete();
         mSupport = false;
      }
   }
}</pre>
複製程式碼

在這個類裡面主要是檢測裝置是否是支援的AndFix的裝置,YunOS不支援。

在SecurityChecker裡面獲取數字證照和檢測應用是否debugable

另外是初始化patch路徑

PachManager.init()裡面獲取AndFix的版本資訊,如果資訊一致

<pre style="margin: 0px; padding: 0px; font-size: 0.85em; font-family: monospace, serif; overflow: auto; line-height: 1.45; background-color: rgb(248, 248, 248); border-radius: 0.5em; position: relative; border: 1px solid rgb(238, 238, 238);">public void init(String appVersion) {
   if (!mPatchDir.exists() && !mPatchDir.mkdirs()) {// make directory fail
      Log.*e*(*TAG*, "patch dir create error.");
      return;
   } else if (!mPatchDir.isDirectory()) {// not directory
      mPatchDir.delete();
      return;
   }
   SharedPreferences sp = mContext.getSharedPreferences(*SP_NAME*,
         Context.*MODE_PRIVATE*);
   String ver = sp.getString(*SP_VERSION*, null);
   if (ver == null || !ver.equalsIgnoreCase(appVersion)) {
      cleanPatch();
      sp.edit().putString(*SP_VERSION*, appVersion).commit();
   } else {
      initPatchs();
   }
}</pre>
複製程式碼

則initPatchs

<pre style="margin: 0px; padding: 0px; font-size: 0.85em; font-family: monospace, serif; overflow: auto; line-height: 1.45; background-color: rgb(248, 248, 248); border-radius: 0.5em; position: relative; border: 1px solid rgb(238, 238, 238);">private void initPatchs() {
   File[] files = mPatchDir.listFiles();
   for (File file : files) {
      addPatch(file);
   }
}</pre>
複製程式碼

把路徑下的patch加到mPatch列表裡面。

<pre style="margin: 0px; padding: 0px; font-size: 0.85em; font-family: monospace, serif; overflow: auto; line-height: 1.45; background-color: rgb(248, 248, 248); border-radius: 0.5em; position: relative; border: 1px solid rgb(238, 238, 238);">patchManager.loadPatch();</pre>

<pre style="margin: 0px; padding: 0px; font-size: 0.85em; font-family: monospace, serif; overflow: auto; line-height: 1.45; background-color: rgb(248, 248, 248); border-radius: 0.5em; position: relative; border: 1px solid rgb(238, 238, 238);">public void loadPatch() {
   mLoaders.put("*", mContext.getClassLoader());// wildcard
   Set<String> patchNames;
   List<String> classes;
   for (Patch patch : mPatchs) {
      patchNames = patch.getPatchNames();
      for (String patchName : patchNames) {
         classes = patch.getClasses(patchName);
         mAndFixManager.fix(patch.getFile(), mContext.getClassLoader(),
               classes);
      }
   }
}</pre>
複製程式碼

最終呼叫fix方法

<pre style="margin: 0px; padding: 0px; font-size: 0.85em; font-family: monospace, serif; overflow: auto; line-height: 1.45; background-color: rgb(248, 248, 248); border-radius: 0.5em; position: relative; border: 1px solid rgb(238, 238, 238);">mAndFixManager.fix(patch.getFile(), mContext.getClassLoader(),
      classes);</pre>
複製程式碼

fix方法首先簽名驗證,一般是檔案的MD5,通過之後獲取dex檔案,

然後實現自己的載入器(只需要繼承ClassLoader,並覆蓋findClass方法)。

<pre style="margin: 0px; padding: 0px; font-size: 0.85em; font-family: monospace, serif; overflow: auto; line-height: 1.45; background-color: rgb(248, 248, 248); border-radius: 0.5em; position: relative; border: 1px solid rgb(238, 238, 238);">ClassLoader patchClassLoader = new ClassLoader(classLoader) {
      @Override
      protected Class<?> findClass(String className)
            throws ClassNotFoundException {
         Class<?> clazz = dexFile.loadClass(className, this);
         if (clazz == null
               && className.startsWith("com.alipay.euler.andfix")) {
            return Class.*forName*(className);// annotation’s class
                                    // not found
         }
         if (clazz == null) {
            throw new ClassNotFoundException(className);
         }
         return clazz;
      }
   };
   Enumeration<String> entrys = dexFile.entries();
   Class<?> clazz = null;
   while (entrys.hasMoreElements()) {
      String entry = entrys.nextElement();
      if (classes != null && !classes.contains(entry)) {
         continue;// skip, not need fix
      }
      clazz = dexFile.loadClass(entry, patchClassLoader);
      if (clazz != null) {
         fixClass(clazz, classLoader);
      }
   }
} catch (IOException e) {
   Log.e(*TAG*, "pacth", e);
}</pre>
複製程式碼

在類載入器裡面,如果知道需要修改的方法(annotation標記的),則呼叫fixClass去修復bug.

<pre style="margin: 0px; padding: 0px; font-size: 0.85em; font-family: monospace, serif; overflow: auto; line-height: 1.45; background-color: rgb(248, 248, 248); border-radius: 0.5em; position: relative; border: 1px solid rgb(238, 238, 238);">private void fixClass(Class<?> clazz, ClassLoader classLoader) {
   Method[] methods = clazz.getDeclaredMethods();
   MethodReplace methodReplace;
   String clz;
   String meth;
   for (Method method : methods) {
      methodReplace = method.getAnnotation(MethodReplace.class);
      if (methodReplace == null)
         continue;
      clz = methodReplace.clazz();
      meth = methodReplace.method();
      if (!*isEmpty*(clz) && !*isEmpty*(meth)) {
         replaceMethod(classLoader, clz, meth, method);
      }
   }
}</pre>
複製程式碼

在fixClass裡面呼叫replaceMethod方法用patch裡面的方法替換掉要修改的方法。

<pre style="margin: 0px; padding: 0px; font-size: 0.85em; font-family: monospace, serif; overflow: auto; line-height: 1.45; background-color: rgb(248, 248, 248); border-radius: 0.5em; position: relative; border: 1px solid rgb(238, 238, 238);">private void replaceMethod(ClassLoader classLoader, String clz,
      String meth, Method method) {
   try {
      String key = clz + "@" + classLoader.toString();
      Class<?> clazz = *mFixedClass*.get(key);
      if (clazz == null) {// class not load
         Class<?> clzz = classLoader.loadClass(clz);
         // initialize target class
         clazz = AndFix.*initTargetClass*(clzz);
      }
      if (clazz != null) {// initialize class OK
         *mFixedClass*.put(key, clazz);
         Method src = clazz.getDeclaredMethod(meth,
               method.getParameterTypes());
         AndFix.*addReplaceMethod*(src, method);
      }
   } catch (Exception e) {
      Log.e(*TAG*, "replaceMethod", e);
   }
}</pre>
複製程式碼

最終呼叫native層的方法

<pre style="margin: 0px; padding: 0px; font-size: 0.85em; font-family: monospace, serif; overflow: auto; line-height: 1.45; background-color: rgb(248, 248, 248); border-radius: 0.5em; position: relative; border: 1px solid rgb(238, 238, 238);">AndFix.*addReplaceMethod*(src, method);</pre>
複製程式碼
<pre style="margin: 0px; padding: 0px; font-size: 0.85em; font-family: monospace, serif; overflow: auto; line-height: 1.45; background-color: rgb(248, 248, 248); border-radius: 0.5em; position: relative; border: 1px solid rgb(238, 238, 238);">public static void addReplaceMethod(Method src, Method dest) {
   try {
      *replaceMethod*(src, dest);
      *initFields*(dest.getDeclaringClass());
   } catch (Throwable e) {
      Log.e(*TAG*, "addReplaceMethod", e);
   }
}</pre>
複製程式碼

Native裡面的replaceMethod和虛擬機器型別有關。

原始碼路徑:github.com/alibaba/And…


相關文章