当前位置:城玮文档网 >作文大全 > 【安卓笔记】,热修复实现x

【安卓笔记】,热修复实现x

时间:2022-08-04 09:15:09 来源:网友投稿

 【安卓笔记】

 热修复实现 热修复实现( 一) 方案有很多种,我就只说明下我想到的方式,也就是 Instant Run 的方式: 分拆到不同的 dex 中,然后通过 classloader 来进行加载。但是在之前InstantRun 详解中只说到会通过内部的 server 去判断该类是否有更新,如果有的话就去从新的 dex 中加载该类,否则就从旧的 dex 中加载,但这是如何实现的呢? 怎么去从不同的 dex 中选择最新的那个来进行加载。

 讲到这里需要先介绍一下 ClassLoader :

 < A class loader is an object that is responsible for loading classes. The class ClassLoader is an abstract class. Given the binary name of a class, a class loader should attempt to locate or generate data that constitutes a definition for the class. A typical strategy is to transform the name into a file name and then read a "class file" of that name from a file system.

 在一般情况下,应用程序不需要创建 ClassLoader 对象,而是使用当前环境已经存在的 ClassLoader 。因为 Java 的 Runtime 环境在初始化时,其内部会创建一个ClassLoader 对象用于加载 Runtime 所需的各种 Java 类。

 每个 ClassLoader 必须有一个父类,在装载 Class 文件时,子 ClassLoader 会先请求父 ClassLoader 加载该 Class 文件,只有当其父 ClassLoader 找不到该 Class文件时,子 ClassLoader 才会继续装载该类,这是一种安全机制。

 对于 Android 的应用程序,本质上虽然也是用 Java 开发,并且使用标准的 Java编译器编译出 Class 文件,但最终的 APK 文件中包含的却是 dex 类型的文件。

 dex文件是将所需的所有 Class 文件重新打包,打包的规则不是简单的压缩,而是完全对 Class 文件内部的各种函数表、变量表等进行优化,并产生一个新的文件,这就是 dex 文件。由于 dex 文件是一种经过优化的 Class 文件,因此要加载这样特殊的 Class 文件就需要特殊的类装载器,这就是 DexClassLoader , Android SDK中提供的 D exClassLoader 类就是出于这个目的。

 总体来说, Android

 默认主要有三个 ClassLoader : • BootClassLoader : 系统启动时创建 Provides an explicit representation of the boot class loader. It sits at the head of the class loader chain and delegates requests to the VM"s internal class loading mechanism.

 • PathClassLoader : 可以加载 /data/app 目录下的 apk ,这也意味着,它只能加载已经安装的 apk ;

 • DexClassLoader : 可以加载文件系统上的 jar 、 dex 、 apk ;可以从 SD 卡中加载未安装的 apk

 通过上面的分析知道,如果用多个 dex 的话肯定会用到 DexClassLoader 类,我们首先来看一下它的源码(这里 插一嘴,源码可以去 googlesource 中找): /**

  * A class loader that loads classes from {@code .jar} and {@code .apk} files

  * containing a {@code classes.dex} entry. This can be used to execute code not

  * installed as part of an application.

  *

  * <p>This class loader requires an application-private, writable directory to

  * cache optimized classes. Use {@code Context.getDir(String, int)} to create

  * such a directory: <pre>

  {@code

  *

  File dexOutputDir = context.getDir("dex", 0);

  * }</pre>

  *

  * <p><strong>Do not cache optimized classes on external storage.</strong>

  * External storage does not provide access controls necessary to protect your

  * application from code injection attacks.

  */

 public class DexClassLoader extends BaseDexClassLoader {

 /**

  * Creates a {@code DexClassLoader} that finds interpreted and native

  * code.

 Interpreted classes are found in a set of DEX files contained

  * in Jar or APK files.

  *

  * <p>The path lists are separated using the character specified by the

  * {@code path.separator} system property, which defaults to {@code :}.

  *

  * @param dexPath the list of jar/apk files containing classes and

  *

  resources, delimited by {@code File.pathSeparator}, which

  *

  defaults to {@code ":"} on Android

  * @param optimizedDirectory directory where optimized dex files

  *

  should be written; must not be {@code null}

  * @param libraryPath the list of directories containing native

  *

  libraries, delimited by {@code File.pathSeparator}; may be

  *

  {@code null}

  * @param parent the parent class loader

  */

 public DexClassLoader(String dexPath, String optimizedDirectory,

 String libraryPath, ClassLoader parent) {

 super(dexPath, new File(optimizedDirectory), libraryPath, parent);

 }

 }

 注释说的太明白了,这里就不翻译了,但是我们并没有找到加载的代码,去它的父类中查找, 因为家在都是从 loadClass() 方法中,所以我们去 ClassLoader 类中看一下loadClass() 方法: /**

  * Loads the class with the specified name. Invoking this method is

  * equivalent to calling {@code loadClass(className, false)}.

  * <p>

  * <strong>Note:</strong> In the Android reference implementation, the

  * second parameter of {@link #loadClass(String, boolean)} is ignored

  * anyway.

  * </p>

  *

  * @return the {@code Class} object.

  * @param className

  *

 the name of the class to look for.

  * @throws ClassNotFoundException

  *

  if the class can not be found.

  */

 public Class<?> loadClass(String className) throws ClassNotFoundException {

 return loadClass(className, false);

 }

  /**

  * Loads the class with the specified name, optionally linking it after

  * loading. The following steps are performed:

  * <ol>

  * <li> Call {@link #findLoadedClass(String)} to determine if the requested

  * class has already been loaded.</li>

  * <li>If the class has not yet been loaded: Invoke this method on the

  * parent class loader.</li>

  * <li>If the class has still not been loaded: Call

  * {@link #findClass(String)} to find the class.</li>

  * </ol>

  * <p>

  * <strong>Note:</strong> In the Android reference implementation, the

  * {@code resolve} parameter is ignored; classes are never linked.

  * </p>

  *

  * @return the {@code Class} object.

  * @param className

  *

 the name of the class to look for.

  * @param resolve

  *

 Indicates if the class should be resolved after loading. This

  *

 parameter is ignored on the Android reference implementation;

  *

 classes are not resolved.

  * @throws ClassNotFoundException

  *

  if the class can not be found.

  */

 protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {

 Class<?> clazz = findLoadedClass(className);

  if (clazz == null) {

 ClassNotFoundException suppressed = null;

 try {

 // 先检查父 ClassLoader 是否已经家在过该类

 clazz = parent.loadClass(className, false);

 } catch (ClassNotFoundException e) {

 suppressed = e;

 }

  if (clazz == null) {

 try {

 // 调用 DexClassLoader.findClass()方法。

 clazz = findClass(className);

 } catch (ClassNotFoundException e) {

 e.addSuppressed(suppressed);

 throw e;

 }

 }

 }

  return clazz;

 }

 上面会调用 DexClassLoader.findClass() 方法,但是 DexClassLoader 没有实现该方法,所以去它的父类 BaseDexClassLoader 中看,接着看一下BaseDexClassLoader 的源码:

 /**

  * Base class for common functionality between various dex-based

  * {@link ClassLoader} implementations.

  */

 public class BaseDexClassLoader extends ClassLoader {

 /** originally specified path (just used for {@code toString()}) */

 private final String originalPath;

 /** structured lists of path elements */

 private final DexPathList pathList;

 /**

  * Constructs an instance.

  *

  * @param dexPath the list of jar/apk files containing classes and

  * resources, delimited by {@code File.pathSeparator}, which

  * defaults to {@code ":"} on Android

  * @param optimizedDirectory directory where optimized dex files

  * should be written; may be {@code null}

  * @param libraryPath the list of directories containing native

  * libraries, delimited by {@code File.pathSeparator}; may be

  * {@code null}

  * @param parent the parent class loader

  */

 public BaseDexClassLoader(String dexPath, File optimizedDirectory,

 String libraryPath, ClassLoader parent) {

 super(parent);

 this.originalPath = dexPath;

 this.pathList =

 new DexPathList(this, dexPath, libraryPath, optimizedDirectory);

 }

 @Override

 protected Class<?> findClass(String name) throws ClassNotFoundException {

 // 从 DexPathList 中找

 Class clazz = pathList.findClass(name);

 if (clazz == null) {

 throw new ClassNotFoundException(name);

 }

 return clazz;

 }

 @Override

 protected URL findResource(String name) {

 return pathList.findResource(name);

 }

 @Override

 protected Enumeration<URL> findResources(String name) {

 return pathList.findResources(name);

 }

 @Override

 public String findLibrary(String name) {

 return pathList.findLibrary(name);

 }

 在 findClass() 方法中我们看到调用了 DexPathList.findClass() 方法: /**

  * A pair of lists of entries, associated with a {@code ClassLoader}.

  * One of the lists is a dex/resource path &mdash; typically referred

  * to as a "class path" &mdash; list, and the other names directories

  * containing native code libraries. Class path entries may be any of:

  * a {@code .jar} or {@code .zip} file containing an optional

  * top-level {@code classes.dex} file as well as arbitrary resources,

  * or a plain {@code .dex} file (with no possibility of associated

  * resources).

  *

  * <p>This class also contains methods to use these lists to look up

  * classes and resources.</p>

  */

 /*package*/ final class DexPathList {

 private static final String DEX_SUFFIX = ".dex";

 private static final String JAR_SUFFIX = ".jar";

 private static final String ZIP_SUFFIX = ".zip";

 private static final String APK_SUFFIX = ".apk";

 /** class definition context */

 private final ClassLoader definingContext;

 /** list of dex/resource (class path) elements */

 // 把 dex 封装成一个数组,每个 Element 代表一个 dex

 private final Element[] dexElements;

 /** list of native library directory elements */

 private final File[] nativeLibraryDirectories;

  // .....

  /**

  * Finds the named class in one of the dex files pointed at by

  * this instance. This will find the one in the earliest listed

  * path element. If the class is found but has not yet been

  * defined, then this method will define it in the defining

  * context that this instance was constructed with.

  *

  * @return the named class or {@code null} if the class is not

  * found in any of the dex files

  */

 public Class findClass(String name) {

 for (Element element : dexElements) {

 DexFile dex = element.dexFile;

 // 遍历数组,拿到第一个就返回

 if (dex != null) {

 Class clazz = dex.loadClassBinaryName(name, definingContext);

 if (clazz != null) {

 return clazz;

 }

 }

 }

 return null;

 }

 }

 从上面的源码中分析,我知道系统会把所有相关的 dex 维护到一个数组中,然后在加载类的时候会从该数组中的第一个元素中取,然后返回。那我们只要保证将我们热修复后的 dex 对应的 Element 放到该数组的第一个位置就可以了,这样系统就会加载我们热修复的 dex 中的类。

 所以方案出来了,只要把有问题的类修复后,放到一个单独的 dex ,然后把该 Dex转换成对应的 Element 后再将该 Element 插入到 dexElements 数组的第一个位置就可以了。那该如何去将其插入到 dexElements 数组的第一个位置呢?-- 暴力反射。

 到这里我感觉初步的思路已经有了: 将补丁作为 dex 发布。

 • 通过反射修改该 dex 所对应的 Element 在数组中的位置。

 但是我也想到肯定还会有类似下面的问题: 资源文 四大组 • 清单文件的处理 虽然我知道没有这么简单,但是我还是决定抱着不作不死的宗旨继续前行。

 好了, demo 走起来。

 怎么生成 dex 文件呢? 这要讲过两部分: • .class -> .jar

 : jar -cvf test.jar com/charon/instantfix_sample/MainActivity.class

 • .jar -> .dex : dx --dex --output=target.jar test.jar

 target.jar 就是包含 .dex 的 jar 包 生成好 dex 后我们为了模拟先将其放到 asset 目录下(实际开发中肯定要从接口中去下载,当然还会有一些版本号的判断等),然后就是将该 dex 转换成 方案中采用的是 MultiDex ,对其进行一部分改造,具体代码: • 添加 dex 文件,并执行 install

 /**

 * 添加 apk 包外的 dex 文件

 * 自动执行 install

 * @param dexFile

 */

 public static void addDexFileAutoInstall(Context context, List<File> dexFile,File optimizedDirectory) {

 if (dexFile != null && !dexFile.isEmpty() &&!dexFiles.contains(dexFile)) {

  dexFiles.addAll(dexFile);

  LogUtil.d(TAG, "add other dexfile");

  installDexFile(context,optimizedDirectory);

  }

 }

 • installDexFile 直接调用 MultiDex 的 installSecondaryDexes 方法 /**

  * 添加 apk 包外的 dex 文件,

  * @param context

  */

 publicstatic void installDexFile(Context context, File optimizedDirectory){

 if (checkValidZipFiles(dexFiles)) {

 try {

 installSecondaryDexes(context.getClassLoader(), optimizedDirectory, dexFiles);

 } catch (IllegalAccessExceptione){

  e.printStackTrace();

 } catch (NoSuchFieldExceptione) {

  e.printStackTrace();

 } catch (InvocationTargetExceptione){

  e.printStackTrace();

 } catch (NoSuchMethodExceptione) {

  e.printStackTrace();

 } catch (IOExceptione) {

  e.printStackTrace();

 }

 }

 }

 • 将 patch.dex 放在所有 dex 最前面 private static voidexpandFieldArray(Object instance, String fieldName, Object[]extraElements) throws NoSuchFieldException, IllegalArgumentException,

 IllegalAccessException {

 Field jlrField = findField(instance, fieldName);

 Object[]original = (Object[]) jlrField.get(instance);

 Object[]combined = (Object[]) Array.newInstance(original.getClass().getComponentType(),original.length + extraElements.length);

 // 将后来的 dex 放在前面,主 dex 放在最后。

 System.arraycopy(extraElements, 0, combined, 0, extraElements.length);

 System.arraycopy(original, 0, combined, extraElements.length,original.length);

 // 原始的 dex 合并,是将主 dex 放在前面,其他的 dex 依次放在后面。

 //System.arraycopy(original, 0, combined, 0, original.length);

 //System.arraycopy(extraElements, 0, combined, original.length,extraElements.length);

 jlrField.set(instance, combined);

 }

 到此将 patch.dex 放进了 Element ,接下来的问题就是加载 Class ,当加载patch.dex 中类的时候,会遇到一个问题,这个问题就是 QQ 空间团队遇到的Class 的 CLASS_ISPREVERIFIED 。具体原因是 dvmResolveClass 这个方法对 Class进行了校验。判断这个要 Resolve 的 class 是否和其引用来自一个 dex 。如果不是,就会遇到问题。

 当引用这和被引用者不在同一个 dex 中就会抛出异常,导致 Resolve 失败。

 QQ 空间团队的方案是阻止所有的 Class 类打上 CLASS_ISPREVERIFIED 来逃过校验,这种方式其实是影响性能。

 我们的方案是和 QQ 团队的类似,但是和 QQ 空间不同的是,我们将fromUnverifiedConstant 设置为 true ,来逃过校验,达到补丁的路径。具体怎么实现呢? 要引用 Cydia Hook 技术来 hook Native dalvik 中 dvmResolveClass 这个方法。有关 Cydia Hook 技术请参考: 官网地址 官方教程 • SDK 下载地址 具体代码如下: //指明要 hook 的 lib :

 MSConfig(MSFilterLibrary,"/system/lib/libdvm.so")

 // 在初始化的时候进行 hook

 MSInitialize {

 LOGD("Cydia Init");

 MSImageRef image;

 //载入 lib

 image = MSGetImageByName("/system/lib/libdvm.so");

 if (image != NULL) {

 LOGD("image is not null");

 void *dexload=MSFindSymbol(image,"dvmResolveClass");

 if(dexload != NULL) {

 LOGD("dexloadis not null");

 MSHookFunction(dexload, (void*)proxyDvmResolveClass, (void**)&dvmResolveClass_Proxy);

 } else{

 LOGD("errorfind dvmResolveClass");

 }

 }

 }

 // 在初始化的时候进行 hook//保留原来的地址

 ClassObject* (*dvmResolveClass_Proxy)(ClassObject* referrer, u4 classIdx, boolfromUnverifiedConstant);

 // 新方法地址

 static ClassObject* proxyDvmResolveClass(ClassObject* referrer, u4 classIdx,bool fromUnverifiedConstant) {

 return dvmResolveClass_Proxy(referrer, classIdx,true);

 }

 说到此处,似乎已经是一个完整的方案了,但在实践中,会发现运行加载类的时候报 preverified 错误,原来在 DexPrepare.cpp ,将 dex 转化成 odex 的过程中,会在 DexVerify.cpp 进行校验, 验证如果直接引用到的类和 clazz 是否在同一个 dex ,如果是,则会打上CLASS_ISPREVERIFIED 标志。通过在所有类( Application 除外,当时还没加载自定义类的代码)的构造函数插入一个对在单独的 dex 的类的引用,就可以解决这个问题。空间使用了 javaassist 进行编译时字节码插入。

 所以为了实现补丁方案,所以必须从这些方法中入手,防止类被打上CLASS_ISPREVERIFIED 标志。

 最终空间的方案是往所有类的构造函数里面插入了一段代码,代码如下:

 if (ClassVerifier.PREVENT_VERIFY) {

 System.out.println(AntilazyLoad.class);

 }

 其中 AntilazyLoad 类会被打包成单独的 antilazy.dex ,这样当安装 apk 的时候,classes.dex 内的类都会引用一个在不相同 dex 中的 AntilazyLoad 类,这样就防止了类被打上 CLASS_ISPREVERIFIED 的标志了,只要没被打上这个标志的类都可以进行打补丁操作。

 然后在应用启动的时候加载进来 AntilazyLoad 类所在的 dex包必须被先加载进来,不然 AntilazyLoad 类会被标记为不存在,即使后续加载了hack.dex 包,那么他也是不存在的,这样屏幕就会出现茫茫多的类 AntilazyLoad找不到的 log 。

 所以 Application 作为应用的入口不能插入这段代码。(因为载入 hack.dex 的代码是在 Application 中 onCreate 中执行的,如果在 Application的构造函数里面插入了这段代码,那么就是在 hack.dex 加载之前就使用该类,该类一次找不到,会被永远的打上找不到的标志) 如何打包补丁包: 1. 空间在正式版本发布的时候,会生成一份缓存文件,里面记录了所有 class 文件的 md5 ,还有一份 mapping 混淆文件。

 2. 在后续的版本中使用 -applymapping 选项,应用正式版本的 mapping 文件,然后计算编译完成后的 class 文件的 md5 和正式版本进行比较,把不相同的 class

 文件打包成补丁包。

 备注:该方案现在也应用到我们的编译过程当中,编译不需要重新打包 dex ,只需要把修改过的类的 class 文件打包成 patch dex ,然后放到 sdcard 下,那么就会让改变的代码生效。

 在 Java 中,只有当两个实例的类名、包名以及加载其的 ClassLoader 都相同,才会被认为是同一种类型。上面分别加载的新类和旧类,虽然包名和类名都完全一样,但是由于加载的 ClassLoader 不同,所以并不是同一种类型,在实际使用中可能会出现类型不符异常。

 同一个 Class =相同的 ClassName + PackageName + ClassLoader

 以上问题在采用动态加载功能的开发中容易出现,请注意。

 通过上面的分析,我们知道使用 ClassLoader 动态加载一个外部的类是非常容易的事情,所以很容易就能实现动态加载新的可执行代码的功能,但是比起一般的Java 程序,在 Android 程序中使用动态加载主要有两个麻烦的问题: • Android 中许多组件类(如 Activity、Service 等)是需要在 Manifest 文件里面注册后才能工作的(系统会检查该组件有没有注册),所以即使动态加载了一个新的组件类进来,没有注册的话还是无法工作; • Res 资源是 Android 开发中经常用到的,而 Android 是把这些资源用对应的R.id 注册好,运行时通过这些 ID 从 Resource 实例中获取对应的资源。如果是运行时动态加载进来的新类,那类里面用到 R.id 的地方将会抛出找不到资源或者用错资源的异常,因为新类的资源 ID 根本和现有的 Resource 实例中保存的资源 ID 对不上; 说到底,抛开虚拟机的差别不说,一个 Android 程序和标准的 Java 程序最大的区别就在于他们的上下文环境 (Context) 不同。

 Android 中,这个环境可以给程序提供组件需要用到的功能,也可以提供一些主题、 Res 等资源,其实上面说到的两个问题都可以统一说是这个环境的问题,而现在的各种 Android 动态加载框架中,核心要解决的东西也正是“如何给外部的新类提供上下文环境”的问题。

 DexClassLoader 的使用方法一般有两种: 从已安装的 apk 中读取 dex

 • 从 apk 文件中读取 dex

 假如有两个 APK ,一个是宿主 APK ,叫作 HOST ,一个是插件 APK ,叫作 Plugin 。Plugin 中有一个类叫 PluginClass ,代码如下: public class PluginClass {

 public PluginClass() {

 Log.d("JG","初始化 PluginClass");

 }

  public int function(int a, int b){

 return a+b;

  }

  }

 现在如果想调用插件 APK 中 PluginClass 内的方法,应该怎么办? 从已安装的 apk 中读取 dex 先来看第一种方法,这种方法必须建一个 Activity ,在清单文件中配置 Action . <activity android:name=".MainActivity">

  <intent-filter>

  <action android:name="com.maplejaw.plugin"/>

  </intent-filter>

 </activity>

 然后在宿主 APK 中如下使用: /**

  * 这种方式用于从已安装的 apk 中读取,必须要有一个 activity,且需要配置 ACTION

  */

 private void useDexClassLoader(){

 //创建一个意图,用来找到指定的 apk

 Intent intent = new Intent("com.maplejaw.plugin");

 //获得包管理器

 PackageManager pm = getPackageManager();

 List<ResolveInfo> resolveinfoes =

 pm.queryIntentActivities(intent, 0);

 if(resolveinfoes.size()==0){

 return;

 }

 //获得指定的 activity 的信息

 ActivityInfo actInfo = resolveinfoes.get(0).activityInfo;

  //获得包名

 String packageName = actInfo.packageName;

 //获得 apk 的目录或者 jar 的目录

 String apkPath = actInfo.applicationInfo.sourceDir;

 //dex 解压后的目录,注意,这个用宿主程序的目录,android 中只允许程序读取写自己

 //目录下的文件

 String dexOutputDir = getApplicationInfo().dataDir;

  //native 代码的目录

 String libPath = actInfo.applicationInfo.nativeLibraryDir;

  //创建类加载器,把 dex 加载到虚拟机中

 DexClassLoader calssLoader = new DexClassLoader(apkPath, dexOutpu

 tDir, libPath,

 this.getClass().getClassLoader());

  //利用反射调用插件包内的类的方法

  try {

 Class<?> clazz = calssLoader.loadClass(packageName+".PluginClass");

  Object obj = clazz.newInstance();

 Class[] param = new Class[2];

 param[0] = Integer.TYPE;

 param[1] = Integer.TYPE;

  Method method = clazz.getMethod("function", param);

  Integer ret = (Integer)method.invoke(obj, 12,34);

  Log.d("JG", "返回的调用结果为:" + ret);

  } catch (Exception e) {

 e.printStackTrace();

 }

 }

 我们安装完两个 APK 后,在宿主中就可以直接调用,调用示例如下。

 public void btnClick(View view){

 useDexClassLoader();

 }

 从 从 apk 文件中读取 dex 这种方法由于并不需要安装,所以不需要通过 Intent 从 activity 中解析信息。换言之,这种方法不需要创建 Activity 。无需配置清单文件。我们只需要打包一个 apk ,然后放到 SD 卡中即可。

 核心代码如下: //apk 路径

 String path=Environment.getExternalStorageDirectory().getAbsolutePath()+"/1.apk";

  private void useDexClassLoader(String path){

  File codeDir=getDir("dex", Context.MODE_PRIVATE);

  //创建类加载器,把 dex 加载到虚拟机中

 DexClassLoader calssLoader = new DexClassLoader(path, codeDir.getAbsolutePath(), null,

 this.getClass().getClassLoader());

  //利用反射调用插件包内的类的方法

  try {

 Class<?> clazz = calssLoader.loadClass("com.maplejaw.plugin.PluginClass");

  Object obj = clazz.newInstance();

 Class[] param = new Class[2];

 param[0] = Integer.TYPE;

 param[1] = Integer.TYPE;

  Method method = clazz.getMethod("function", param);

  Integer ret = (Integer)method.invoke(obj, 12,21);

  Log.d("JG", "返回的调用结果为: " + ret);

  } catch (Exception e) {

 e.printStackTrace();

 }

  动态加载的几个关键问题: • 资源访问:无法找到某某 id 所对应的资源 因为将 apk 加载到宿主程序中去执行,就无法通过宿主程序的 Context 去取到 apk 中的资源,比如图片、文本等,这是很好理解的,因为 apk 已经不存在上下文了,它执行时所采用的上下文是宿主程序的上下文, 用别人的 Context 是无法得到自己的资源的 – 解决方案一:插件中的资源在宿主程序中也预置一份; 缺点:增加了宿主 apk 的大小;在这种模式下,每次发布一个插件都需要将资源复制到宿主程序中,这意味着每发布一个插件都要更新一下宿主程序; – 解决方案二:将插件中的资源解压出来,然后通过文件流去读取资源; 缺点:实际操作起来还是有很大难度的。首先不同资源有不同的文件流格式,比如图片、XML 等,其次针对不同设备加载的资源可能是不一样的,如何选择合适的资源也是一个需要解决的问题; 实际解决方案: Activity 中有一个叫 mBase 的成员变量,它的类型就是 ContextImpl 。注意到 Context 中有如下两个抽象方法,看起来是和资源有关的,实际上 Context 就是通过它们来获取资源的。这两个抽象方法的真正实现在 ContextImpl 中;

 /** Return an AssetManager instance for your application"s package. */

 public abstract AssetManager getAssets();

  /** Return a Resources instance for your application"s package. */

 public abstract Resources getResources();

 具体实现: protected void loadResources() {

 try {

 AssetManager assetManager = AssetManager.class.newInstance();

 Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);

 addAssetPath.invoke(assetManager, mDexPath);

 mAssetManager = assetManager;

 } catch (Exception e) {

 e.printStackTrace();

 }

 Resources superRes = super.getResources();

 mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(),

 superRes.getConfiguration());

 mTheme = mResources.newTheme();

 mTheme.setTo(super.getTheme());

 }

  加载资源的方法是通过反射,通过调用 AssetManager 中的 addAssetPath 方法,我们可以将一个 apk 中的资源加载到 Resources 对象中,由于 addAssetPath 是隐藏 API 我们无法直接调用,所以只能通过反射。

 @hide

 public final int addAssetPath(String path) {

 synchronized (this) {

 int res = addAssetPathNative(path);

 makeStringBlocks(mStringBlocks);

 return res;

 }

 }

 Activity 生命周期的管理:反射方式和接口方式。

 反射的方式很好理解,首先通过 Java 的反射去获取 Activity 的各种生命周期方法,比如 onCreate 、 onStart 、 onResume 等,然后在代理 Activity 中去调用插件Activity 对应的生命周期方法即可: 缺点:一方面是反射代码写起来比较复杂,另一方面是过多使用反射会有一定的性能开销。

 反射方式

 @Override

 protected void onResume() {

 super.onResume();

 Method onResume = mActivityLifecircleMethods.get("onResume");

 if (onResume != null) {

 try {

 onResume.invoke(mRemoteActivity, new Object[] { });

 } catch (Exception e) {

 e.printStackTrace();

 }

 }

 }

  @Override

 protected void onPause() {

 Method onPause = mActivityLifecircleMethods.get("onPause");

 if (onPause != null) {

 try {

 onPause.invoke(mRemoteActivity, new Object[] { });

 } catch (Exception e) {

 e.printStackTrace();

 }

 }

 super.onPause();

 }

 接口方式 public interface DLPlugin {

  public void onStart();

  public void onRestart();

  public void onActivityResult(int requestCode, int resultCode, Intent

  data);

  public void onResume();

  public void onPause();

  public void onStop();

  public void onDestroy();

  public void onCreate(Bundle savedInstanceState);

 public void setProxy(Activity proxyActivity, String dexPath);

  public void onSaveInstanceState(Bundle outState);

  public void onNewIntent(Intent intent);

  public void onRestoreInstanceState(Bundle savedInstanceState);

  public boolean onTouchEvent(MotionEvent event);

  public boolean onKeyUp(int keyCode, KeyEvent event);

  public void onWindowAttributesChanged(LayoutParams params);

  public void onWindowFocusChanged(boolean hasFocus);

  public void onBackPressed();

  …

  }

 代理 Activity 中只需要按如下方式即可调用插件 Activity 的生命周期方法,这就完成了插件 Activity 的生命周期的管理;插件 Activity 需要实现 DLPlugin 接口: @Override

 protected void onStart() {

 mRemoteActivity.onStart();

 super.onStart();

 }

  @Override

 protected void onRestart() {

 mRemoteActivity.onRestart();

 super.onRestart();

 }

  @Override

 protected void onResume() {

 mRemoteActivity.onResume();

 super.onResume();

 }

相关热词搜索: 修复 笔记