概述
关联文章
纸上得来终觉浅,绝知此事要躬行,读了这么多源码是时候实践一下了
代码修复
首先我们定义一个简单的类
public class Text {
public static String message(){
return "明天不放假";
}
}
然后再Activity中调用
public void onClick(View v) {
switch (v.getId()) {
case R.id.code:
Toast.makeText(MainActivity.this, Text.message(), Toast.LENGTH_LONG).show();
break;
点击按钮就会弹出明天不放假
的消息,我们的目标把明天不放假
修复为No,明天放假
第一步 制作补丁包
首先改代码
public class Text {
public static String message(){
return "No,明天放假";
}
}
然后把Text.java 编译成class
在命令行输入 javac Text.java
这样你就拿到了class文件
然后把你的class文件放入和包名一样的文件夹
把你的文件夹打成jar包
在命令行输入 jar -cvf patch.jar com
把你的jar包执行dex命令
在命令行输入 dx --dex --output=patch_dex.jar patch.jar
最后把你制作的补丁包放入sdcard中
写代码
我们已经说过原理,就不在重复说了,不知道的可以看我之前的文章Android 热修复原理解析
public static void patch_class(Context context) {
try {
//获取ClassLoader
ClassLoader classLoader = MainActivity.class.getClassLoader();
//获取BaseDexClassLoader的class对象
Class<?> superclass = classLoader.getClass().getSuperclass();
//反射获取BaseDexClassLoader的变量pathList
Field baseDexpathList = superclass.getDeclaredField("pathList");
//设置可访问
baseDexpathList.setAccessible(true);
//获取当前内存中的PathClassLoader的pathList对象
Object pathlist = baseDexpathList.get(classLoader);
//获取DexpathList中的dexElements成员变量
Field dexElementsFiled = pathlist.getClass().getDeclaredField("dexElements");
//设置可访问
dexElementsFiled.setAccessible(true);
//获取当前pathList中的dexElements数组对象
Object[] dexElements = (Object[]) dexElementsFiled.get(pathlist);
//获取pathList中的makeDexElements方法
Method makeDexElements = pathlist.getClass().getDeclaredMethod("makeDexElements",
List.class, File.class, List.class, ClassLoader.class);
makeDexElements.setAccessible(true);
//构建第一个参数,补丁包
ArrayList<File> files = new ArrayList<>();
//获取补丁包
File file = new File(copyFile("/sdcard/patch_dex.jar", context));
files.add(file);
//构建第二个参数,指定解压目录
File optimizedDirectory = new File(context.getFilesDir().getAbsolutePath() +
File.separator + "patch");
if (!optimizedDirectory.exists()) {
optimizedDirectory.mkdirs();
}
//构建第三个参数
ArrayList<IOException> suppressedExceptions = new ArrayList<IOException>();
//执行makeDexElements方法,获取补丁包的dexElement数组
Object[] patchdexElements = (Object[]) makeDexElements.invoke(pathlist, files,
optimizedDirectory, suppressedExceptions, classLoader);
//创建一个数组
Object[] finalArray = (Object[]) Array.newInstance(dexElements.getClass().getComponentType(),
dexElements.length + patchdexElements.length);
//把补丁包放到刚创建的数组
System.arraycopy(patchdexElements, 0, finalArray, 0, patchdexElements.length);
//把原先的数组放入新建的数组
System.arraycopy(dexElements, 0, finalArray, patchdexElements.length, dexElements.length);
//把新数组替换掉原先的数组
dexElementsFiled.set(pathlist, finalArray);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
主要就是把你制作的补丁包解析为数组,然后和原数组合并,并且把你的补丁包放在数组的第一位,这样就会先加载你修复过的文件
效果
sc.png
sc.png
需要注意的地方
不能即时生效:如果你先加载的bug类,再点击代码修复
按钮是没有效果的,建议需要卸载apk,在重新安装,因为你的bug类加载后,就不会加载你修复的类,具体可以看这个,JVM 类加载机制
资源修复
原理也是之前讲过了,不懂的可以看
我们先写一个正常的类加载资源图片
把你的资源放入文件夹中,然后代码加载
public void onClick(View v) {
switch (v.getId()) {
case R.id.resource:
mImageView.setImageResource(R.mipmap.aaaa);
break;
准备补丁包
这个没什么好说的,直接把你需要改的图片替换掉就行,比如我们改成了这个
然后build apk,拿到改过的apk,推入sdcard
这样就完成了
写代码
public static void monkeyPatchExistingResources(@Nullable Context context,
@Nullable String externalResourceFile,
@Nullable Collection<Activity> activities) {
if (externalResourceFile == null) {
return;
}
try {
//利用反射创建一个新的AssetManager
AssetManager newAssetManager = AssetManager.class.getConstructor().newInstance();
//利用反射获取addAssetPath方法
Method mAddAssetPath = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
mAddAssetPath.setAccessible(true);
//利用反射调用addAssetPath方法加载外部的资源(SD卡)
if (((Integer) mAddAssetPath.invoke(newAssetManager, externalResourceFile)) == 0) {
throw new IllegalStateException("Could not create new AssetManager");
}
// Kitkat needs this method call, Lollipop doesn't. However, it doesn't seem to cause any harm
// in L, so we do it unconditionally.
Method mEnsureStringBlocks = AssetManager.class.getDeclaredMethod("ensureStringBlocks");
mEnsureStringBlocks.setAccessible(true);
mEnsureStringBlocks.invoke(newAssetManager);
if (activities != null) {
//遍历activities
for (Activity activity : activities) {
//拿到Activity的Resources
Resources resources = activity.getResources();
try {
//获取Resources的成员变量mAssets
Field mAssets = Resources.class.getDeclaredField("mAssets");
mAssets.setAccessible(true);
//给成员变量mAssets重新赋值为自己创建的newAssetManager
mAssets.set(resources, newAssetManager);
} catch (Throwable ignore) {
Field mResourcesImpl = Resources.class.getDeclaredField("mResourcesImpl");
mResourcesImpl.setAccessible(true);
Object resourceImpl = mResourcesImpl.get(resources);
Field implAssets = resourceImpl.getClass().getDeclaredField("mAssets");
implAssets.setAccessible(true);
implAssets.set(resourceImpl, newAssetManager);
}
//获取activity的theme
Resources.Theme theme = activity.getTheme();
try {
try {
//反射得到Resources.Theme的mAssets变量
Field ma = Resources.Theme.class.getDeclaredField("mAssets");
ma.setAccessible(true);
//将Resources.Theme的mAssets替换成newAssetManager
ma.set(theme, newAssetManager);
} catch (NoSuchFieldException ignore) {
Field themeField = Resources.Theme.class.getDeclaredField("mThemeImpl");
themeField.setAccessible(true);
Object impl = themeField.get(theme);
Field ma = impl.getClass().getDeclaredField("mAssets");
ma.setAccessible(true);
ma.set(impl, newAssetManager);
}
Field mt = ContextThemeWrapper.class.getDeclaredField("mTheme");
mt.setAccessible(true);
mt.set(activity, null);
Method mtm = ContextThemeWrapper.class.getDeclaredMethod("initializeTheme");
mtm.setAccessible(true);
mtm.invoke(activity);
Method mCreateTheme = AssetManager.class.getDeclaredMethod("createTheme");
mCreateTheme.setAccessible(true);
Object internalTheme = mCreateTheme.invoke(newAssetManager);
Field mTheme = Resources.Theme.class.getDeclaredField("mTheme");
mTheme.setAccessible(true);
mTheme.set(theme, internalTheme);
} catch (Throwable e) {
Log.e("mmm", "Failed to update existing theme for activity " + activity,
e);
}
}
}
代码没啥说的,上篇文章已经说过了
效果
sc.png
sc.png
资源修复
首先准备俩个so
,一个是有bug
的,一个是修复的,怎么制作so
我就不展开讲了,复习一下ndk就行了,把你需要修复的libnative-lib_patch.so
推入到sdcard
中
写一个简单的代码,加载so
public void onClick(View v) {
switch (v.getId()) {
case R.id.so:
Toast.makeText(MainActivity.this, SoUtils.getKey(), Toast.LENGTH_LONG).show();
break;
SoUtils.getKey
调用so中的方法,打印出字符串
写代码
public static void patch_so(Context context){
try {
String localPath = "/sdcard/libnative-lib_patch.so";
Log.v("mmm", "LazyBandingLib localPath:" + localPath);
// 开辟一个输入流
File inFile = new File(localPath);
// 判断需加载的文件是否存在
if (!inFile.exists()){
System.loadLibrary("native-lib");
return;
}
FileInputStream fis = new FileInputStream(inFile);
File dir = context.getDir("libs", Context.MODE_PRIVATE);
// 获取驱动文件输出流
File soFile = new File(dir,"libnative-lib.so");
if (!soFile.exists()) {
Log.v("mmm", "### " + soFile.getAbsolutePath() + " is not exists");
FileOutputStream fos = new FileOutputStream(soFile);
Log.v("mmm", "FileOutputStream:" + fos.toString());
// 字节数组输出流,写入到内存中(ram)
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = -1;
while ((len = fis.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
// 从内存到写入到具体文件
fos.write(baos.toByteArray());
// 关闭文件流
baos.close();
fos.close();
}
fis.close();
Log.v("mmm", "### System.load start");
// 加载外设驱动
System.load(soFile.getAbsolutePath());
Log.v("mmm", "### System.load End");
} catch (Exception e) {
e.printStackTrace();
}
}
so修复我选择了加载so方法的替换
的形式来写,主要逻辑,如果sdcard
中有补丁包,则加载补丁包,如果没有则加载APK中的so
效果
sc.png
sc.png
注意事项
需要先点击一下SO修复
按钮,要先把so包加载才行
总结
我的代码运行在华为7.0机器,因为各个的不同的版本,源码细节也会不同,所以其他的版本可能会遇到bug,这就需要自己去看一下源码去解决了,正好也能锻炼一下实际操作能力
Demo已经传到GitHub,如果有帮助希望能给点一个star
我把用到的修复资源,放到了assets文件夹下,直接取出,放入到sdcard中即可
参考: