现在的热修复方案已经有很多了,例如alibaba的dexposed、AndFix以及jasonross的Nuwa等等。原来没有仔细去分析过也没想写这篇文章,但是之前InstantRun详解这篇文章中介绍了Android Studio Instant Run的
实现原理,这不就是活生生的一个热修复吗? 随心情久久不能平复,我们何不用这种方式来实现。
方案有很多种,我就只说明下我想到的方式,也就是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中提供的DexClassLoader类就是出于这个目的。
总体来说,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 — typically referred
* to as a "class path" — 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.jartarget.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技术请参考:
具体代码如下:
//指明要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, dexOutputDir, 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();
}
- Google Instant app
- 微信热补丁实现
- 多dex分拆
- QQ空间热修复方案
- Android dex分包方案
- 类加载器DexClassLoader
- 基于cydia Hook在线热修复补丁方案
- Android 热补丁动态修复框架小结
- 美团Android DEX自动拆包及动态加载简介
- 插件化从放弃到捡起
- 无需Root也能使用Xposed!
- 当你准备开发一个热修复框架的时候,你需要了解的一切
- 邮箱 :charon.chui@gmail.com
- Good Luck!