热修复
热修复
装载请注明出处:https://blog.csdn.net/xiaowu_zhu/article/details/79792533
热修复,其实已经不是一个新技术了,目前发展的也有好几种方案了,奈何,以前我并没有使用过,只是做了大致的了解,最近看了某位大神的简书,然后自己动手实践了吧,分析了哈原理,再次分享哈。
在一次的版本发布后,突然发现某了某个小bug,或者优化了一些东西,我们不可能再次去发布新版本,频繁发布版本,用户更新成本大, 并且用户有可能不会立马去更新,所以才有了热修复的开发流程来代替传统的开发流程。
热修复开发流程
版本1.0上线 >> 用户安装 >> 发现bug >> 紧急修复 >> 打出补丁,推送给用户>> 自动拉取补丁修复
热修复的开发流程显得更加灵活,优势很多:
- 无需重新发版,实时高效热修复
- 用户无感知修复,无需下载新的应用,代价小
- 修复成功率高,把损失降到最低
业界热门的修复技术
热修复作为当下热门的技术,在业界内比较著名的有阿里巴巴的AndFix、Dexposed,腾讯QQ空间的超级补丁技术和微信的Tinker。最近阿里百川推出的HotFix热修复服务就基于AndFix技术,定位于线上紧急BUG的即时修复。具体的细节请看:www.infoq.com/cn/articles/Android-hot-fix
目前我使用过的方案有: 阿里的AndFix和基于ClassLoader的修复方案。
阿里的AndFix
源码:AndFix
简介
AndFix 是阿里的一套热修复方案,它目前只能修复方法。
Android 使用方式
gradle
dependencies { compile 'com.alipay.euler:andfix:0.5.0@aar' }
在
Application
中初始化PatchManger
patchManager = new PatchManager(context); patchManager.init(appversion);//current version
加载Patch
patchManager.loadPatch();
动态加载Patch
String patchPath = Environment.getExternalStorageDirectory() + File.separator + "fix.apatch"; File file = new File(patchPath); if (file.exists()) { try { BaseApplication.sPatchManager.addPatch(patchPath); Toast.makeText(this, "修复成功", Toast.LENGTH_SHORT).show(); } catch (IOException e) { e.printStackTrace(); Toast.makeText(this, "修复失败", Toast.LENGTH_SHORT).show(); } }
注意:在本地加载时,一定要注意添加存储的读取权限和动态权限。
代码混淆
如果需要代码混淆,请在混淆规则中加入:
-keep class * extends java.lang.annotation.Annotation -keepclasseswithmembernames class * { native <methods>; }
生成差分包
去github上下载 差分包生成工具
将两个apk拷贝到工具包中的目录下
通过命令生成
.apatch
包usage: apkpatch -f <new> -t <old> -o <output> -k <keystore> -p <***> -a <alias> -e <***> -f: 没有bug的新版本 -t: 有bug的旧版本 -o:生成差分包`.apatch`的位置 -k: apk打包的签名文件 -p:打包签名文件的密码 -a:签名密钥的别名 -e:签名密钥别名的密码
例如:apkpatch.bat -f new.apk -t old.apk -o out -k joke.jks -p 123456 -a joke -e 123456
差分包的源码
通过命令生成的差分包,通过重命名为.zip
,提取.dex
,通过反编译工具,我们可以看到:
@MethodReplace(clazz="cn.xwj.androidfix.MainActivity", method="test")
public void test(View paramView){
Toast.makeText(this, "test: 0", 1).show();
}
通过命令工具,比对出是哪个方法被修改了,然后就会添加一个@MethodReplace
注解,然后通过调用native方法。在andfix.cpp
中:
static void replaceMethod(JNIEnv* env, jclass clazz, jobject src,
jobject dest) {
if (isArt) {
art_replaceMethod(env, src, dest);
} else {
dalvik_replaceMethod(env, src, dest);
}
}
4.4 的替换方法,可以看到通过底层替换了修改的方法的指向。
extern void __attribute__ ((visibility ("hidden"))) dalvik_replaceMethod(
JNIEnv* env, jobject src, jobject dest) {
jobject clazz = env->CallObjectMethod(dest, jClassMethod);
ClassObject* clz = (ClassObject*) dvmDecodeIndirectRef_fnPtr(
dvmThreadSelf_fnPtr(), clazz);
clz->status = CLASS_INITIALIZED;
Method* meth = (Method*) env->FromReflectedMethod(src);
Method* target = (Method*) env->FromReflectedMethod(dest);
LOGD("dalvikMethod: %s", meth->name);
// meth->clazz = target->clazz;
meth->accessFlags |= ACC_PUBLIC;
meth->methodIndex = target->methodIndex;
meth->jniArgInfo = target->jniArgInfo;
meth->registersSize = target->registersSize;
meth->outsSize = target->outsSize;
meth->insSize = target->insSize;
meth->prototype = target->prototype;
meth->insns = target->insns;
meth->nativeFunc = target->nativeFunc;
}
开发中的注意事项
- 每次生成之后一定要测试;
- 尽量的不要分包,不要分多个dex
- 混淆的时候,设计到NDK AndFix.java 不要混淆
- 生成包之后一般会加固什么的,这个时候生成的差分包,一定要在之前去生成。
- 既然是去修复方法,第一个不能增加成员变量,不能增加方法
- 如果下载到本地去添加的话,一定要注意添加权限和6.0之后动态生成权限
基于ClassLoader方式(Tinker)
Activity加载流程
- Activity
@Override
public void startActivity(Intent intent) {
this.startActivity(intent, null);
}
@Override
public void startActivity(Intent intent, @Nullable Bundle options) {
if (options != null) {
startActivityForResult(intent, -1, options);
} else {
// Note we want to go through this call for compatibility with
// applications that may have overridden the method.
startActivityForResult(intent, -1);
}
}
public void startActivityForResult(@RequiresPermission Intent intent, int requestCode,
@Nullable Bundle options) {
if (mParent == null) {
options = transferSpringboardActivityOptions(options);
//Instrumentation#execStartActivity
Instrumentation.ActivityResult ar =
mInstrumentation.execStartActivity(
this, mMainThread.getApplicationThread(), mToken, this,
intent, requestCode, options);
.....
}
......
}
- Instrumentation
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
.......
try {
intent.migrateExtraStreamToClipData();
intent.prepareToLeaveProcess(who);
// ActivityManagerService#startActivity
int result = ActivityManager.getService()
.startActivity(whoThread, who.getBasePackageName(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()),
token, target != null ? target.mEmbeddedID : null,
requestCode, 0, null, options);
checkStartActivityResult(result, intent);
} catch (RemoteException e) {
throw new RuntimeException("Failure from system", e);
}
return null;
}
public static IActivityManager getService() {
return IActivityManagerSingleton.get(); //得到的就是ActivityManager
}
//单例
private static final Singleton<IActivityManager> IActivityManagerSingleton =
new Singleton<IActivityManager>() {
@Override
protected IActivityManager create() {
final IBinder b = ServiceManager.getService(Context.ACTIVITY_SERVICE);
final IActivityManager am = IActivityManager.Stub.asInterface(b);
return am;
}
};
然后会调用IBinder底层方法,进行Activity的启动,这里就不做太深的描述了,以后谈到Activity的启动流程的时候,通过源码,再详细看看。
通过IBinder底层方法后,会调用ActivityThread
方法来启动Activity
。
ActivityThread
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) { // System.out.println("##### [" + System.currentTimeMillis() + "] ActivityThread.performLaunchActivity(" + r + ")"); ....... Activity activity = null; try { //通过ClassLoader加载Activity java.lang.ClassLoader cl = appContext.getClassLoader(); //newActivity activity = mInstrumentation.newActivity( cl, component.getClassName(), r.intent); StrictMode.incrementExpectedActivityCount(activity.getClass()); r.intent.setExtrasClassLoader(cl); r.intent.prepareToEnterProcess(); if (r.state != null) { r.state.setClassLoader(cl); } } catch (Exception e) { if (!mInstrumentation.onException(activity, e)) { throw new RuntimeException( "Unable to instantiate activity " + component + ": " + e.toString(), e); } } ..... return activity; }
Instrumentation
public Activity newActivity(ClassLoader cl, String className,Intent intent) throws InstantiationException, IllegalAccessException,ClassNotFoundException { //通过classLoader加载class return (Activity)cl.loadClass(className).newInstance(); }
通过寻找
appContext.getClassLoader()
方法,我们可以知道,系统会去创建一个系统的ClassLoader@Override public ClassLoader getClassLoader() { //不管是mClassLoader,mPackageInfo.getClassLoader,最后调用的都是ClassLoader.getSystemCloader()获取的classLoader一样 return mClassLoader != null ? mClassLoader : (mPackageInfo != null ? mPackageInfo.getClassLoader() : ClassLoader.getSystemClassLoader()); }
ClassLoader
private static ClassLoader createSystemClassLoader() { String classPath = System.getProperty("java.class.path", "."); String librarySearchPath = System.getProperty("java.library.path", ""); //PathClassLoader return new PathClassLoader(classPath, librarySearchPath, BootClassLoader.getInstance()); }
从上面的代码可以看出,
ClassLoader.getSystemClassLoader()
创建的是一个PathClassLoader
实例。PathClassLoader
PathClassLoader继承了BaseDexClassLoader
public class PathClassLoader extends BaseDexClassLoader { public PathClassLoader(String dexPath, ClassLoader parent) { super((String)null, (File)null, (String)null, (ClassLoader)null); throw new RuntimeException("Stub!"); } public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) { super((String)null, (File)null, (String)null, (ClassLoader)null); throw new RuntimeException("Stub!"); } }
BaseDexClassLoader
public class BaseDexClassLoader extends ClassLoader { public BaseDexClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent) { throw new RuntimeException("Stub!"); } //这个就是我们需要的findClass protected Class<?> findClass(String name) throws ClassNotFoundException { throw new RuntimeException("Stub!"); } .... }
由于BaseDexClassLoader在我们下载的Android SDK是隐藏的,所以我们需要去查看没有阉割版的源码。在这里我提供两个地址:
通过源码,我们可以看到:
public class BaseDexClassLoader extends ClassLoader { /* @NonNull */ private static volatile Reporter reporter = null; private final DexPathList pathList; public BaseDexClassLoader(String dexPath, File optimizedDirectory, String librarySearchPath, ClassLoader parent) { super(parent); this.pathList = new DexPathList(this, dexPath, librarySearchPath, null); if (reporter != null) { reporter.report(this.pathList.getDexPaths()); } } public BaseDexClassLoader(ByteBuffer[] dexFiles, ClassLoader parent) { // TODO We should support giving this a library search path maybe. super(parent); this.pathList = new DexPathList(this, dexFiles); } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { List<Throwable> suppressedExceptions = new ArrayList<Throwable>(); Class c = pathList.findClass(name, suppressedExceptions); if (c == null) { ClassNotFoundException cnfe = new ClassNotFoundException( "Didn't find class \"" + name + "\" on path: " + pathList); for (Throwable t : suppressedExceptions) { cnfe.addSuppressed(t); } throw cnfe; } return c; } }
BaseDexClassLoader调用的
findClass()
中调用了DexPathList
中的findClass()
。DexPathList
final class DexPathList { private Element[] dexElements; public Class<?> findClass(String name, List<Throwable> suppressed) { for (Element element : dexElements) { Class<?> clazz = element.findClass(name, definingContext, suppressed); if (clazz != null) { //遍历dexElements,找到就立马返回 return clazz; } } if (dexElementsSuppressedExceptions != null) { suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions)); } return null; } }
Activity加载核心
- 类:PathDexClassLoader —> BaseDexClassLoader —> ClassLoader
- 方法: BaseDexClassLoader.findClass() –> DexPathList.findClass()—> 遍历dexElements
自定义实现
public void addFixDex(@NonNull String dexPath) throws Exception {
//判断路径是否为空
if (TextUtils.isEmpty(dexPath)) {
throw new IllegalArgumentException("dexPath is null");
}
//判断文件是否存在
File src = new File(dexPath);
if (!src.exists()) {
throw new FileNotFoundException(dexPath);
}
File dest = new File(mDexRootDir, src.getName());
//判断文件是否已经被加载
if (dest.exists()) {
Log.d(TAG, "dex [" + dexPath + "] has already loaded");
return;
}
//将文件拷贝到指定的目录
FileUtils.copyFile(src, dest);
//获取app的classLoader
ClassLoader appClassLoader = mContext.getClassLoader();
//获取BaseDexClassLoader中的pathList 字段
Field pathListField = BaseDexClassLoader.class.getDeclaredField("pathList");
pathListField.setAccessible(true); //设置可访问
Object pathList = pathListField.get(appClassLoader); //得到pathList的值
//通过反射获取DexPathList中的dexElements字段
Field dexElementsField = pathList.getClass().getDeclaredField("dexElements");
dexElementsField.setAccessible(true); //设置可访问
Object dexElements = dexElementsField.get(pathList);//得到dexElements
//创建dex的解压目录
File optimizedDirectory = new File(mDexRootDir + File.separator + "dex");
if (!optimizedDirectory.exists()) {//不存在需要创建
optimizedDirectory.mkdirs(); //创建
}
//创建一个BaseDexClassLoader
BaseDexClassLoader classLoader = new BaseDexClassLoader(
dest.getAbsolutePath(), //dex的路径
optimizedDirectory,
null, //library的解压目录
appClassLoader //app的classLoader
);
//通过classLoader获取fixDex中的pathList的值
Object fixDexPathList = pathListField.get(classLoader);
//通过fixDexPathList获取PathDexList中的dexElements中的值
Object fixDexElements = dexElementsField.get(fixDexPathList);
//和并dexElements
Object object = combineDexElements(dexElements, fixDexElements);
//将合并的重新设置给appClassLoader所在的dexElements中
dexElementsField.set(pathList, object);
Log.d(TAG, "完成");
}
上面只是一个初略的实现。具体代码:androidFix
个人博客:https://xiaowujiang.cn 欢迎访问。