转载请标明出处:
http://blog.csdn.net/hai_qing_xu_kong/article/details/70284239
本文出自:【顾林海的博客】
前言
当热修复框架还没出现时,我们的整个开发流程是这样的:先是开发,接着测试,如果有bug修复,当测试实在测不出问题,就打包上线,如果在线上出现问题,就需要修复Bug,并再次打包上线,由于各大平台的审核机制不同,上线的时间也是不固定,在这个阶段用户在多次打开APP并出现相同问题后就有可能卸载软件,这样的话公司就会流失部分用户,在热修复出现后,可以避免这种情况的发生,因为线上出现bug后,我们可以通过热修复来修复Bug,不用每次出现Bug都要重新上架。市面上的热修复有很多,像Nuwa、微信的Tinker以及阿里百川HotFix,这篇文章讲述Nuwa的使用以及相关的原理。
集成热修复框架Nuwa
步骤一:
工程根目录中添加:
classpath 'cn.jiajixin.nuwa:gradle:1.2.2'
最后工程根目录下的build.gradle是这样的:
buildscript {
repositories {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:1.2.0'
classpath 'cn.jiajixin.nuwa:gradle:1.2.2'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
步骤二:
在app下的build.gradle添加依赖:
apply plugin: "cn.jiajixin.nuwa"
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
testCompile 'junit:junit:4.12'
compile 'com.android.support:appcompat-v7:24.2.1'
compile 'cn.jiajixin.nuwa:nuwa:1.0.0'//添加nuwa sdk
}
步骤三:
在app下的build.gradle中dubug和release开启混淆
在AndroidManifest.xml中添加权限:
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
步骤四:
创建项目的Application并添加到AndroidManifest.xml中,在Application中添加:
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
Nuwa.init(this);
Nuwa.loadPatch(this, Environment.getExternalStorageDirectory().getAbsolutePath().concat("/patch.jar"));
}
使用热修复框架Nuwa
ok,整体流程已经结束,现在我们编写一个有bug的app,我先在MainActivity添加以下代码:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// findViewById(R.id.tv_show).setOnClickListener(new View.OnClickListener() {
// @Override
// public void onClick(View v) {
// Toast.makeText(MainActivity.this, "nuwa", Toast.LENGTH_SHORT).show();
// }
// });
}
}
这段代码中tv_show是不可点击的,我们点击run这个项目,并在安装在手机上,这时我们查看app/build/outputs文件下会出现一个nuwa的文件夹,我们看看这个文件夹下有些什么:
我们看的有两个文件,这两个文件在后面会用到,我们将整个nuwa文件夹复制到某个路径下。
接着我们修改上面MainActivity,修改内容如下:
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.tv_show).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(MainActivity.this, "nuwa", Toast.LENGTH_SHORT).show();
}
});
}
}
上面我将注释去掉,也就是点击这个TextView会出现弹窗,我们打开android studio 的Terminal窗口,输入以下内容:
gradlew clean nuwaDebugPatch -P NuwaDir=F:/glhproject/nuwa/nuwa
上面的F:/glhproject/nuwa/nuwa就是之前我们复制的nuwa文件夹。
这时我们再查看app/build/outputs会发现多了一个叫patch.jar的东西:
在日常开发中,这个patch.jar是需要放在服务器上,通过推送或接口调用来下载这个patch.jar文件,我们这里直接复制到手机的sdcard上:
adb push app/build/outputs/nuwa/debug/patch.jar /sdcard/
最后的最后我们重启app,这样我们第二次修改的内容就生效了。
热修复框架Nuwa原理
在一头埋入nuwa源码前,我们先来扫扫盲,聊聊PathClassLoader,这货有什么用,PathClassLoader的作用就是从文件系统中加载类文件:
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!");
}
}
PathClassLoader继承BaseDexClassLoader,在BaseDexClassLoader中有个findClass方法:
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new RuntimeException("Stub!");
}
查看findClass方法的具体实现:
@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;
}
在findClass方法中调用了pathList对象的findClass方法,pathLit的类型是DexPathList,查看DexPathList中的findClass方法:
public Class findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
在findClass方法中,通过循环遍历dexElements,在循环遍历中,调用loadClassBinaryName方法来加载,加载成功就返回这个Class对象。讲到这里,我们就知道,dexElements的先后顺序是非常重要,决定着哪个dex被加载,因此要想实现热修复,就需要我们将修复后的dex文件放在dexElements前面,使得这个dex文件能先被加载,从而达到热修复。
Nuwa的原理就是利用了上面的dexElements来加载修复完的dex文件,我查看到Nuwa的源码其实比较少的:
调用 Nuwa.init(this):
public static void init(Context context) {
File dexDir = new File(context.getFilesDir(), DEX_DIR);
dexDir.mkdir();
String dexPath = null;
try {
dexPath = AssetUtils.copyAsset(context, HACK_DEX, dexDir);
} catch (IOException e) {
Log.e(TAG, "copy " + HACK_DEX + " failed");
e.printStackTrace();
}
loadPatch(context, dexPath);
}
在init方法中,创建nuwa文件,并从assets目录中拷贝一个叫hack.apk空实现的文件到nuwa文件中,在调用下面方法来进行热修复,方法内的第二个参数就是我们在上面生成的patch.jar的路径。
Nuwa.loadPatch(this, Environment.getExternalStorageDirectory().getAbsolutePath().concat("/patch.jar"));
接着调用loadPatch方法,查看此方法:
public static void loadPatch(Context context, String dexPath) {
if (context == null) {
Log.e(TAG, "context is null");
return;
}
if (!new File(dexPath).exists()) {
Log.e(TAG, dexPath + " is null");
return;
}
File dexOptDir = new File(context.getFilesDir(), DEX_OPT_DIR);
dexOptDir.mkdir();
try {
DexUtils.injectDexAtFirst(dexPath, dexOptDir.getAbsolutePath());
} catch (Exception e) {
Log.e(TAG, "inject " + dexPath + " failed");
e.printStackTrace();
}
}
在这个方法中,先是进行两次判空,分别是context和我们存放修复后的dex文件否存在,接着创建一个nuwaopt文件夹,接着调用DexUtils中的静态方法injectDexAtFirst:
public static void injectDexAtFirst(String dexPath, String defaultDexOptPath) throws NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
DexClassLoader dexClassLoader = new DexClassLoader(dexPath, defaultDexOptPath, dexPath, getPathClassLoader());
Object baseDexElements = getDexElements(getPathList(getPathClassLoader()));
Object newDexElements = getDexElements(getPathList(dexClassLoader));
Object allDexElements = combineArray(newDexElements, baseDexElements);
Object pathList = getPathList(getPathClassLoader());
ReflectionUtils.setField(pathList, pathList.getClass(), "dexElements", allDexElements);
}
获取DexClassLoader实例,DexClassLoader的作用是动态的装载class文件,并且DexClassLoader继承与BaseDexClassLoader,接着通过反射获取到DexPathList属性对象pathList,getDexElements方法中通过反射获取dexElements,上面两个baseDexElements和newDexElements分别是当前的dexElements和补丁dex的dexElements,随后将两个dexElements进行合并:
private static Object combineArray(Object firstArray, Object secondArray) {
Class<?> localClass = firstArray.getClass().getComponentType();
int firstArrayLength = Array.getLength(firstArray);
int allLength = firstArrayLength + Array.getLength(secondArray);
Object result = Array.newInstance(localClass, allLength);
for (int k = 0; k < allLength; ++k) {
if (k < firstArrayLength) {
Array.set(result, k, Array.get(firstArray, k));
} else {
Array.set(result, k, Array.get(secondArray, k - firstArrayLength));
}
}
return result;
}
将patch.dex放在最前面,最后加载Element数组,来完成修复。
虽然Nuwa框架有很多优点,但由于它不是即时生效,并且修复的类过大,会导致加载时间延长,以及在ART模式下,类修改了结构,会导致内存错乱,如果想解决这个问题,就需要将相关的调用类、父类、子类等都加载到patch.dex中,会导致补丁过大。