自己动手实现 Android App 插件化

qoes4901 7年前
   <p>Android插件化目前国内已经有很多开源的工程了,不过如果不实际开发一遍,很难掌握的很好。</p>    <p>下面是自己从0开始,结合目前开源的项目和博客,动手开发插件化方案。</p>    <p>按照需要插件化主要解决下面的几种问题:</p>    <p>1. 代码的加载</p>    <p>(1) 要解决纯Java代码的加载</p>    <p>(2) Android组件加载,如Activity、Service、Broadcast Receiver、ContentProvider,因为它们是有生命周期的,所以要特殊处理</p>    <p>(3) Android Native代码的加载</p>    <p>(4) Android 特殊控件的处理,如Notification等</p>    <p>2. 资源加载</p>    <p>不同插件的资源如何管理,是公用一套还是插件独立管理?</p>    <p>因为在Android中访问资源,都是通过R. 实现的,</p>    <p>下面就一步步解决上面的问题</p>    <p>1. 纯Java代码的加载</p>    <p>主要就是通过ClassLoader、更改DexElements将插件的路径添加到原来的数组中。</p>    <p>详细的分析可以参考我转载的一篇文章,因为感觉原贴命名和结构有点乱,所以转载记录下。</p>    <p>https://my.oschina.net/android520/blog/794715</p>    <p>Android提供DexClassLoader和PathClassLoader,都继承BaseDexClassLoader,只是构造方法的参数不一样,即optdex的路径不一样,源码如下</p>    <pre>  <code class="language-java">// DexClassLoader.java  public class DexClassLoader extends BaseDexClassLoader {      public DexClassLoader(String dexPath, String optimizedDirectory,              String libraryPath, ClassLoader parent) {          super(dexPath, new File(optimizedDirectory), libraryPath, parent);      }  }    // PathClassLoader.java  public class PathClassLoader extends BaseDexClassLoader {      public PathClassLoader(String dexPath, ClassLoader parent) {          super(dexPath, null, null, parent);      }        public PathClassLoader(String dexPath, String libraryPath,              ClassLoader parent) {          super(dexPath, null, libraryPath, parent);      }  }</code></pre>    <p>其中,optimizedDirectory是用来存储opt后的dex目录,必须是内部存储路径。</p>    <p>DexClassLoader可以加载外部的dex或apk,只要opt的路径通过参数设置一个内部存储路径即可。</p>    <p>PathClassLoader只能加载已安装的apk,因为opt路径会使用默认的dex路径,外部的不可以。</p>    <p>下面介绍下如何通过DexClassLoader实现加载Java代码,参考Nuwa</p>    <p>这种方式类似于热修复,如果插件和宿主代码有相互访问,则需要在打包中使用插桩技术实现。</p>    <pre>  <code class="language-java">public static boolean injectDexAtFirst(String dexPath, String dexOptPath) {        // 获取系统的dexElements      Object baseDexElements = getDexElements(getPathList(getPathClassLoader()));        // 获取patch的dexElements      DexClassLoader patchDexClassLoader = new DexClassLoader(dexPath, dexOptPath, dexPath, getPathClassLoader());      Object patchDexElements = getDexElements(getPathList(patchDexClassLoader));        // 组合最新的dexElements      Object allDexElements = combineArray(patchDexElements, baseDexElements);        // 将最新的dexElements添加到系统的classLoader中      Object pathList = getPathList(getPathClassLoader());      FieldUtils.writeField(pathList, "dexElements", allDexElements);  }    public static ClassLoader getPathClassLoader() {      return DexUtils.class.getClassLoader();  }    /**   * 反射调用getPathList方法,获取数据   * @param classLoader   * @return   * @throws ClassNotFoundException   * @throws NoSuchFieldException   * @throws IllegalAccessException   */  public static Object getPathList(ClassLoader classLoader) throws ClassNotFoundException, NoSuchFieldException, IllegalAccessException {      return FieldUtils.readField(classLoader, "pathList");  }    /**   * 反射调用pathList对象的dexElements数据   * @param pathList   * @return   * @throws NoSuchFieldException   * @throws IllegalAccessException   */  public static Object getDexElements(Object pathList) throws NoSuchFieldException, IllegalAccessException {      LogUtils.d("Reflect To Get DexElements");      return FieldUtils.readField(pathList, "dexElements");  }    /**   * 拼接dexElements,将patch的dex插入到原来dex的头部   * @param firstElement   * @param secondElement   * @return   */  public static Object combineArray(Object firstElement, Object secondElement) {        LogUtils.d("Combine DexElements");        // 取得一个数组的Class对象, 如果对象是数组,getClass只能返回数组类型,而getComponentType可以返回数组的实际类型      Class objTypeClass = firstElement.getClass().getComponentType();        int firstArrayLen = Array.getLength(firstElement);      int secondArrayLen = Array.getLength(secondElement);      int allArrayLen = firstArrayLen + secondArrayLen;        Object allObject = Array.newInstance(objTypeClass, allArrayLen);      for (int i = 0; i < allArrayLen; i++) {          if (i < firstArrayLen) {              Array.set(allObject, i, Array.get(firstElement, i));          } else {              Array.set(allObject, i, Array.get(secondElement, i - firstArrayLen));          }      }      return allObject;  }</code></pre>    <p>使用上面的方式启动的Activity,是有生命周期的,应该是使用系统默认的创建Activity方式,而不是自己new Activity对象,所以打开的Activity生命周期正常。</p>    <p>但是上面的方式,必须保证Activity在宿主AndroidManifest.xml中注册。</p>    <p>2. 下面介绍下如何加载未注册的Activity功能</p>    <p>Activity的加载原理参考 https://my.oschina.net/android520/blog/795599</p>    <p>主要通过Hook系统的IActivityManager完成</p>    <p>3. 资源加载</p>    <p>资源访问都是通过R.方式,实际上Android会生成一个0x7f******格式的int常量值,关联对应的资源。</p>    <p>如果资源有更改,如layout、id、drawable等变化,会重新生成R.java内容,int常量值也会变化。</p>    <p>因为插件中的资源没有参与宿主程序的资源编译,所以无法通过R.进行访问。</p>    <p>具体原理参照 https://my.oschina.net/android520/blog/796346</p>    <p>使用addAssetPath方式将插件路径添加到宿主程序后,因为插件是独立打包的,所以资源id也是从1开始,而宿主程序也是从1开始,可能会导致插件和宿主资源冲突,系统加载资源时以最新找到的资源为准,所以无法保证界面展示的是宿主的,还是插件的。</p>    <p>针对这种方式,可以在打包时,更改每个插件的资源id生成的范围,可以参考public.xml介绍。</p>    <p>代码参考Amigo</p>    <pre>  <code class="language-java">public static void loadPatchResources(Context context, String apkPath) throws Exception {      AssetManager newAssetManager = AssetManager.class.newInstance();      invokeMethod(newAssetManager, "addAssetPath", apkPath);      invokeMethod(newAssetManager, "ensureStringBlocks");      replaceAssetManager(context, newAssetManager);  }    private static void replaceAssetManager(Context context, AssetManager newAssetManager)              throws Exception {      Collection<WeakReference<Resources>> references;      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {          Class<?> resourcesManagerClass = Class.forName("android.app.ResourcesManager");          Object resourcesManager = invokeStaticMethod(resourcesManagerClass, "getInstance");            if (getField(resourcesManagerClass, "mActiveResources") != null) {              ArrayMap<?, WeakReference<Resources>> arrayMap =                          (ArrayMap) readField(resourcesManager, "mActiveResources", true);              references = arrayMap.values();          } else {              references = (Collection) readField(resourcesManager, "mResourceReferences", true);          }      } else {          HashMap<?, WeakReference<Resources>> map =                      (HashMap) readField(ActivityThreadCompat.instance(), "mActiveResources", true);          references = map.values();      }        AssetManager assetManager = context != null ? context.getAssets() : null;      for (WeakReference<Resources> wr : references) {          Resources resources = wr.get();          if (resources == null) continue;            try {              writeField(resources, "mAssets", newAssetManager);              originalAssetManager = assetManager;          } catch (Throwable ignore) {              Object resourceImpl = readField(resources, "mResourcesImpl", true);              writeField(resourceImpl, "mAssets", newAssetManager);          }            resources.updateConfiguration(resources.getConfiguration(),                      resources.getDisplayMetrics());      }        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {          for (WeakReference<Resources> wr : references) {              Resources resources = wr.get();              if (resources == null) continue;                // android.util.Pools$SynchronizedPool<TypedArray>              Object typedArrayPool = readField(resources, "mTypedArrayPool", true);                // Clear all the pools              while (invokeMethod(typedArrayPool, "acquire") != null) ;          }      }  }</code></pre>    <p> </p>    <p>来自:https://my.oschina.net/android520/blog/796350</p>    <p> </p>