热修复设计之CLASS_ISPREVERIFIED(二)
阿里P7移动互联网架构师进阶视频(每日更新中)免费学习请点击:https://space.bilibili.com/474380680
本篇文章将继续从CLASS_ISPREVERIFIED实战来介绍热修复设计:
一、前言
本文将解决两个问题。
- 怎么将修复后的Bug类打包成dex
- 怎么将外部的dex插入到ClassLoader中
二、建立测试Demo
2.1 目录结构
2.2 源码
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
tools:context=".MainActivity">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="click"
android:text="小喵叫一声"/>
</RelativeLayout>
MainActivity.class
package com.aitsuki.bugfix;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.view.View;
import android.widget.Toast;
import com.aitsuki.bugfix.animal.Cat;
public class MainActivity extends AppCompatActivity {
private Cat mCat;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mCat = new Cat();
}
public void click(View view) {
Toast.makeText(this, mCat.say(),Toast.LENGTH_SHORT).show();
}
}
Cat.class
package com.aitsuki.bugfix.animal;
/**
* Created by AItsuki on 2016/3/14.
*/
public class Cat {
public String say() {
return "汪汪汪!";
}
}
2.3 运行结果
假设这是我们公司的开发项目,刚刚上线就发现了严重bug,猫会狗叫。
想修复bug,让用户再立刻更新一次显然很不友好,此时热补丁修复技术就有用了。
三、制作补丁
在加载dex的代码之前,我们先来制作补丁。
1. 首先我们将Cat类修复,汪汪汪改成喵喵喵,然后重新编译项目。(Rebuild一下就行了)
2. 去保存项目的地方,将Cat.class文件拷贝出来,在这里
3. 新建文件夹,要和该Cat.class文件的包名一致,然后将Cat.class复制到这里,如图
4. 命令行进入到图中的test目录,运行一下命令,打包补丁。如图:
然后test目录是这样的
patch_dex.jar就是我们打包好的补丁了,我们将它放到sdCard中,待会从这里加载补丁。
关于什么用这么复杂的方法打包补丁的说明:
你也可以直接将java文件拷出来,通过javac -d带包编译再转成jar。
但我这么麻烦是有原因的,因为用这种方法你可能会遇到ParseException,原因是jar包版本和dx工具版本不一致。
而从项目中直接将编译好的class直接转成jar就没问题,因为java会向下兼容,打出来的jar包和class版本是一致的。
总而言之,dx版本要和class编译版本对应。
四、加载补丁
4.1 思路
通过上一篇博文,我们知道dex保存在这个位置
BaseDexClassLoader–>pathList–>dexElements
apk的classes.dex可以从应用本身的DexClassLoader中获取。
path_dex的dex需要new一个DexClassLoader加载后再获取。
分别通过反射取出dex文件,重新合并成一个数组,然后赋值给盈通本身的ClassLoader的dexElements
4.2 代码实现
加载外部dex,我们可以在Application中操作。
首先新建一个HotPatchApplication,然后在清单文件中配置,顺便加上读取sdcard的权限,因为补丁就保存在那里。
HotPatchApplication代码如下:
package com.aitsuki.hotpatchdemo;
import android.app.Application;
import android.os.Environment;
import android.util.Log;
import java.io.File;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import dalvik.system.DexClassLoader;
/**
* Created by hp on 2016/4/6.
*/
public class HotPatchApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
// 获取补丁,如果存在就执行注入操作
String dexPath = Environment.getExternalStorageDirectory().getAbsolutePath().concat("/patch_dex.jar");
File file = new File(dexPath);
if (file.exists()) {
inject(dexPath);
} else {
Log.e("BugFixApplication", dexPath + "不存在");
}
}
/**
* 要注入的dex的路径
*
* @param path
*/
private void inject(String path) {
try {
// 获取classes的dexElements
Class<?> cl = Class.forName("dalvik.system.BaseDexClassLoader");
Object pathList = getField(cl, "pathList", getClassLoader());
Object baseElements = getField(pathList.getClass(), "dexElements", pathList);
// 获取patch_dex的dexElements(需要先加载dex)
String dexopt = getDir("dexopt", 0).getAbsolutePath();
DexClassLoader dexClassLoader = new DexClassLoader(path, dexopt, dexopt, getClassLoader());
Object obj = getField(cl, "pathList", dexClassLoader);
Object dexElements = getField(obj.getClass(), "dexElements", obj);
// 合并两个Elements
Object combineElements = combineArray(dexElements, baseElements);
// 将合并后的Element数组重新赋值给app的classLoader
setField(pathList.getClass(), "dexElements", pathList, combineElements);
//======== 以下是测试是否成功注入 =================
Object object = getField(pathList.getClass(), "dexElements", pathList);
int length = Array.getLength(object);
Log.e("BugFixApplication", "length = " + length);
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
/**
* 通过反射获取对象的属性值
*/
private Object getField(Class<?> cl, String fieldName, Object object) throws NoSuchFieldException, IllegalAccessException {
Field field = cl.getDeclaredField(fieldName);
field.setAccessible(true);
return field.get(object);
}
/**
* 通过反射设置对象的属性值
*/
private void setField(Class<?> cl, String fieldName, Object object, Object value) throws NoSuchFieldException, IllegalAccessException {
Field field = cl.getDeclaredField(fieldName);
field.setAccessible(true);
field.set(object, value);
}
/**
* 通过反射合并两个数组
*/
private Object combineArray(Object firstArr, Object secondArr) {
int firstLength = Array.getLength(firstArr);
int secondLength = Array.getLength(secondArr);
int length = firstLength + secondLength;
Class<?> componentType = firstArr.getClass().getComponentType();
Object newArr = Array.newInstance(componentType, length);
for (int i = 0; i < length; i++) {
if (i < firstLength) {
Array.set(newArr, i, Array.get(firstArr, i));
} else {
Array.set(newArr, i, Array.get(secondArr, i - firstLength));
}
}
return newArr;
}
}
五、CLASS_ISPREVERIFIED
运行一下Demo,报以下错误。(AndroidStudio 2.0可能不会报错,需要打包的时候才会出现错误,这是Instant run导致的)
dexElements的length = 2,看来我们的patch_dex已经成功添加进去了。
但是从黄色框框和黄色框上面那一段log提示中可以看出,MainActivity引用了Cat,但是发现他们在不同的Dex中。
看到这里可能就会问:
为什么之前那么多项目都采用分包方案,但是却不会出现这个错误呢?
我在这里总结了一个过程,想知道详细分析过程的请看QQ空间开发团队的原文。
在apk安装的时候,虚拟机会将dex优化成odex后才拿去执行。在这个过程中会对所有class一个校验。
校验方式:假设A该类在它的static方法,private方法,构造函数,override方法中直接引用到B类。如果A类和B类在同一个dex中,那么A类就会被打上CLASS_ISPREVERIFIED标记
被打上这个标记的类不能引用其他dex中的类,否则就会报图中的错误
在我们的Demo中,MainActivity和Cat本身是在同一个dex中的,所以MainActivity被打上了CLASS_ISPREVERIFIED。而我们修复bug的时候却引用了另外一个dex的Cat.class,所以这里就报错了
而普通分包方案则不会出现这个错误,因为引用和被引用的两个类一开始就不在同一个dex中,所以校验的时候并不会被打上CLASS_ISPREVERIFIED
补充一下第二条:A类如果还引用了一个C类,而C类在其他dex中,那么A类并不会被打上标记。换句话说,只要在static方法,构造方法,private方法,override方法中直接引用了其他dex中的类,那么这个类就不会被打上CLASS_ISPREVERIFIED标记。
5.1 解决方案
根据上面的第六条,我们只要让所有类都引用其他dex中的某个类就可以了。
下面是QQ控件给出的解决方案
在所有类的构造函数中插入这行代码 System.out.println(AntilazyLoad.class);
这样当安装apk的时候,classes.dex内的类都会引用一个在不相同dex中的AntilazyLoad类,这样就防止了类被打上CLASS_ISPREVERIFIED的标志了,只要没被打上这个标志的类都可以进行打补丁操作。
hack.dex在应用启动的时候就要先加载出来,不然AntilazyLoad类会被标记为不存在,即使后面再加载hack.dex,AntilazyLoad类还是会提示不存在。该类只要一次找不到,那么就会永远被标上找不到的标记了。
我们一般在Application中执行dex的注入操作,所以在Application的构造中不能加上System.out.println(AntilazyLoad.class);这行代码,因为此时hack.dex还没有加载进来,AntilazyLoad并不存在。
之所以选择构造函数是因为他不增加方法数,一个类即使没有显式的构造函数,也会有一个隐式的默认构造函数。
5.2 插入代码的难点
1.首先在源码中手动插入不太可行,hack.dex此时并没有加载进来,AntilazyLoad.class并不存在,编译不通过。
2.所以我们需要在源码编译成字节码之后,在字节码中进行插入操作。对字节码进行操作的框架有很多,但是比较常用的则是ASM和javaassist
3.但AndroidStudio是使用Gradle构建项目,编译-打包都是自动化的,我们怎么操作呢。
阿里P7移动互联网架构师进阶视频(每日更新中)免费学习请点击:https://space.bilibili.com/474380680
原文链接:https://blog.csdn.net/u010386612/article/details/51077291
热修复设计之CLASS_ISPREVERIFIED(二)的更多相关文章
- 热修复设计之AOT/JIT&dexopt 与 dex2oat (一)
阿里P7移动互联网架构师进阶视频(每日更新中)免费学习请点击:https://space.bilibili.com/474380680本篇文章将先从AOT/JIT&dexopt 与 dex2o ...
- 前端通信:SSE设计方案(二)--- 服务器推送技术的实践以及一些应用场景的demo(包括在线及时聊天系统以及线上缓存更新,代码热修复案例)
距离上一篇博客,这篇文章的发布大概过了整整三个月.我也从饿了么度过了试用期,成为了正式员工.刚进来恰好遇到项目底层改造和迁移,将项目从angular全部迁移到vue上,所以适应这边的节奏和业务的开发任 ...
- android--------微信 Tinker 热修复 (二)
前面简单介绍了一下Tinker热修复,今天就来分享一下如何在Android中使用,希望对各位有帮助. 1:Tinker 接入指南 在项目的build.gradle中,添加tinker-patch-gr ...
- 阿里最新热修复Sophix与QQ超级补丁和Tinker的实现与总结
2015年以来,Android开发领域里对热修复技术的讨论和分享越来越多,同时也出现了一些不同的解决方案,如QQ空间补丁方案.阿里AndFix以及微信Tinker(Bugly sdk也集成Tikner ...
- 关于iOS热修复(HotPatch)技术的使用总结
苹果做了非常多的努力来建造和维持一个健康并且干净的应用环境.其中对现在的现状起到很大作用的部分就是苹果APP STORE,它是被一个十分周密的对所有提交的应用进行检查的审批程序所保护的.尽管这个程序是 ...
- Android热修复之微信Tinker使用初探
文章地址:Android热修复之微信Tinker使用初探 前几天,万众期待的微信团队的Android热修复框架tinker终于在GitHub上开源了. 地址:https://github.com/ ...
- Android热修复技术选型(不在市场发布新版本的情况下,直接更新app)
2015年以来,Android开发领域里对热修复技术的讨论和分享越来越多,同时也出现了一些不同的解决方案,如QQ空间补丁方案.阿里AndFix以及微信Tinker,它们在原理各有不同,适用场景各异,到 ...
- 全面了解Android热修复技术
WeTest 导读 本文探讨了Android热修复技术的发展脉络,现状及其未来. 热修复技术概述 热修复技术在近年来飞速发展,尤其是在InstantRun方案推出之后,各种热修复技术竞相涌现.国内大部 ...
- Unity3D游戏xlua轻量级热修复框架
这是什么东西 前阵子刚刚集成xlua到项目,目的只有一个:对线上游戏C#逻辑有Bug的地方执行修复,通过考察了xlua和tolua,最终选择了xlua,原因如下: 1)项目已经到了后期,线上版本迭代了 ...
随机推荐
- 正则finditer的使用
import re #\. 是刚需必须有 d+ 必须一个或多个数字 pattern = re.compile(r'\d+\.\d*') d = pattern.finditer('3.14159265 ...
- PAT甲级【2019年3月考题】——A1159 Structure_of_a_BinaryTree【30】
Suppose that all the keys in a binary tree are distinct positive integers. Given the postorder and i ...
- VB - FSO的常见对象和方法
1. set fs=wscript.createobject(“scripting.filesystemobject”) 这样就建立了fso的模型.如果要释放的话也很简单, set fs=nothin ...
- Redis测试类
单机&集群安装: https://blog.csdn.net/zxd1435513775/article/details/88901992 安装5.0.4版本OK,5.0.5版本make时报错 ...
- Python运行出现:ModuleNotFoundError: No module named 'logbook',解决办法
ModuleNotFoundError: No module named 'xlrd' ModuleNotFoundError: No module named 'xlwt' AttributeErr ...
- C# 与 C++ 互操作(C# 调用 C++ 的动态链接库)
1.新建 C++ 动态链接库项目 CPlus.cpp: #include "stdafx.h" extern "C" __declspec(dllexport) ...
- 选择器与过滤器(全)————JQ
JQ基础--选择器与过滤器(全) JQ选择器 <!DOCTYPE html> <html> <head> <meta charset="UTF-8& ...
- SQL数据库—<6>存储过程
Transact-SQL中的存储过程,非常类似于Java语言中的方法,它可以重复调用.当存储过程执行一次后,可以将语句缓存中,这样下次执行的时候直接使用缓存中的语句.这样就可以提高存储过程的性能. Ø ...
- 2018-2-22-在-windows-安装-Jekyll
title author date CreateTime categories 在 windows 安装 Jekyll lindexi 2018-02-22 17:47:39 +0800 2018-2 ...
- Redis数据库总结
1.Redis简介 REmote DIctionary Server(Redis) 是一个由Salvatore Sanfilippo写的key-value存储系统.Redis是一个开源的使用ANSI ...