主目录见:Android高级进阶知识(这是总目录索引)
框架地址:AndFix
在线源码查看:AndroidXRef
我们前面讲了插件化的框架,今天我们来讲讲我们的热修复框架,首先在选热修复框架的时候有犹豫过,到底是要讲美团的robust,微信的Tinker,还是阿里的AndFix等等那么这里用一张图来做一个开始:
热修复框架对比这里是缺少了美团的robust,还有阿里的最新热修复框架Sophix(这个框架也有借鉴了AndFix的思想)的对比,但是我们来想想,热修复最可能出现的时候应该是方法逻辑的修复,而且修复包要尽量小且能即时生效,同时AndFix用C++中的指针思想来做方法的替换,这个代价也是非常小的。在这几个方面来看看,AndFix还是很理想的。
一.AndFix的使用
怎么使用我们其实可以从GitHub的[AndFix]开源地址获取,但是这里还是列举一下:
1.增加dependency依赖
dependencies {
compile 'com.alipay.euler:andfix:0.5.0@aar'
}
2.初始化PatchManager
patchManager = new PatchManager(context);
patchManager.init(appversion);//current version
3.加载补丁包
patchManager.loadPatch();
框架推荐你要尽可能早地加载补丁包,最好是在应用的初始化阶段例如
Application.onCreate())
4.添加补丁包
patchManager.addPatch(path);//path of the patch file that was downloaded
当一个新的补丁包下载下来,通过addPath
方法可以即时生效
5.补丁包生成
AndFix框架提供了一个补丁包生成工具apkpatch
,可以到这里下载apkpatch。
使用的时候要准备两个apk文件,其中一个是修复过的,然后生成.patch补丁文件:
usage: apkpatch -f <new> -t <old> -o <output> -k <keystore> -p <***> -a <alias> -e <***>
-a,--alias <alias> keystore entry alias.
-e,--epassword <***> keystore entry password.
-f,--from <loc> new Apk file path.
-k,--keystore <loc> keystore path.
-n,--name <name> patch name.
-o,--out <dir> output dir.
-p,--kpassword <***> keystore password.
-t,--to <loc> old Apk file path.
然后就可以把这个补丁包分发给客户端了。
有时候团队的成员会修复各自的bug,会生成不止一个补丁包,这样我们可以合并这多个补丁包:
usage: apkpatch -m <apatch_path...> -o <output> -k <keystore> -p <***> -a <alias> -e <***>
-a,--alias <alias> keystore entry alias.
-e,--epassword <***> keystore entry password.
-k,--keystore <loc> keystore path.
-m,--merge <loc...> path of .apatch files.
-n,--name <name> patch name.
-o,--out <dir> output dir.
-p,--kpassword <***> keystore password.
好啦,使用起来并不是很难,我们接下来就从使用来分析源码了。
二.AndFix框架源码解析
从使用我们知道,我们的程序会首先初始化PatchManager,所以我们看看初始化做了啥:
public PatchManager(Context context) {
mContext = context;
//初始化AndFixManager
mAndFixManager = new AndFixManager(mContext);
//初始化存放patch补丁文件的目录
mPatchDir = new File(mContext.getFilesDir(), DIR);
//初始化存在Patch类的集合
mPatchs = new ConcurrentSkipListSet<Patch>();
//初始化存放类对应的类加载器集合
mLoaders = new ConcurrentHashMap<String, ClassLoader>();
}
这里面有个比较重要的类AndFixManager
,我们来看看它的构造函数:
public AndFixManager(Context context) {
mContext = context;
//兼容性判断,判断AndFix是否适用
mSupport = Compat.isSupport();
if (mSupport) {
//主要是对补丁包的安全检测
mSecurityChecker = new SecurityChecker(mContext);
//初始化补丁包的路径
mOptDir = new File(mContext.getFilesDir(), DIR);
if (!mOptDir.exists() && !mOptDir.mkdirs()) {// make directory fail
//如果不定目录不存在则把支持标志设为false
mSupport = false;
Log.e(TAG, "opt dir create error.");
} else if (!mOptDir.isDirectory()) {// not directory
//如果不是目录则标志设置为false
mOptDir.delete();
mSupport = false;
}
}
}
这个类构造函数做的工作很简单,就是Android系统兼容性的检查,然后就是对补丁包安全性和路径的检验。接着我们看看兼容性判断源码:
public static synchronized boolean isSupport() {
if (isChecked)
return isSupport;
isChecked = true;
// not support alibaba's YunOs
//写的很清楚,这里不支持阿里的YunOs,且会判断Davilk还是art虚拟机,来注册native方法
if (!isYunOS() && AndFix.setup() && isSupportSDKVersion()) {
isSupport = true;
}
//是否在黑名单中,默认是返回false
if (inBlackList()) {
isSupport = false;
}
return isSupport;
}
判断是不是YunOs系统和支持的Sdk版本这两个方法比较简单,这里不深究,重点来看AndFix.setup()
方法:
public static boolean setup() {
try {
final String vmVersion = System.getProperty("java.vm.version");
boolean isArt = vmVersion != null && vmVersion.startsWith("2");
int apilevel = Build.VERSION.SDK_INT;
return setup(isArt, apilevel);
} catch (Exception e) {
Log.e(TAG, "setup", e);
return false;
}
}
这个方法也是很简单,判断vm的版本是不是以2开头,如果是就是art虚拟机,然后将isArt和api的版本传给了native的setup
方法,这个方法在jni目录的AndFix.cpp
下面:
static jboolean setup(JNIEnv* env, jclass clazz, jboolean isart,
jint apilevel) {
isArt = isart;
LOGD("vm is: %s , apilevel is: %i", (isArt ? "art" : "dalvik"),
(int )apilevel);
if (isArt) {
return art_setup(env, (int) apilevel);
} else {
return dalvik_setup(env, (int) apilevel);
}
}
可以看到方法里面根据isArt这个标志来调用不同的方法,首先我们看下art_setup
方法:
extern jboolean __attribute__ ((visibility ("hidden"))) art_setup(JNIEnv* env,
int level) {
apilevel = level;
return JNI_TRUE;
}
可以看出这个方法就记录了下apilevel然后直接返回了true。接着我们来看看dalvik_setup
方法:
extern jboolean __attribute__ ((visibility ("hidden"))) dalvik_setup(
JNIEnv* env, int apilevel) {
//打开libdvm.so文件
void* dvm_hand = dlopen("libdvm.so", RTLD_NOW);
if (dvm_hand) {
//获取dvmDecodeIndirectRef_fnPtr和dvmThreadSelf_fnPtr两个函数,
//这两个函数可以通过类对象获取ClassObject结构体
dvmDecodeIndirectRef_fnPtr = dvm_dlsym(dvm_hand,
apilevel > 10 ?
"_Z20dvmDecodeIndirectRefP6ThreadP8_jobject" :
"dvmDecodeIndirectRef");
if (!dvmDecodeIndirectRef_fnPtr) {
return JNI_FALSE;
}
dvmThreadSelf_fnPtr = dvm_dlsym(dvm_hand,
apilevel > 10 ? "_Z13dvmThreadSelfv" : "dvmThreadSelf");
if (!dvmThreadSelf_fnPtr) {
return JNI_FALSE;
}
//这里用jni里面的方法来获取Java层Method对象的getDeclaringClass方法
//后续会调用该方法获取某个方法所属的类对象
//因为Java层只传递了Method对象到native层
jclass clazz = env->FindClass("java/lang/reflect/Method");
jClassMethod = env->GetMethodID(clazz, "getDeclaringClass",
"()Ljava/lang/Class;");
return JNI_TRUE;
} else {
return JNI_FALSE;
}
}
这里面我们先来看看Android虚拟机初始化方面的知识图:
Android虚拟机初始化
1.跟java虚拟机不同,Android虚拟机执行的是dex文件而不是class文件,同时在android版本4.4开始就推出了art虚拟机,所以现在android的虚拟机分为dalvik虚拟机
和art虚拟机
。
2.虚拟机启动的时候会加载两个很重要的动态库文件libdvm.so
和libart.so
,这就是dalvik虚拟机
和art虚拟机
的动态库文件
3.Java在虚拟机环境中执行,每个Java方法都会对应一个底层的函数指针,当Java方法被调用的时候,实质虚拟机会找到这个函数指针然后去执行底层的方法,从而Java方法被执行。
接着我们继续来分析dalvik_setup
方法,首先这个方法会判断apilevel是不是大于10来Hook不同的系统函数,在Hook成功了之后,我们就可以调用我们自己的代码进行替换,这就是框架github上面说的:
替换方法我们留着后面具体说,我们接着来看PatchManager
类中的init
方法:
public void init(String appVersion) {
//首先判断补丁路径是否存在或者是不是目录
if (!mPatchDir.exists() && !mPatchDir.mkdirs()) {// make directory fail
Log.e(TAG, "patch dir create error.");
return;
} else if (!mPatchDir.isDirectory()) {// not directory
mPatchDir.delete();
return;
}
//这里用sp来存储补丁包的配置信息
SharedPreferences sp = mContext.getSharedPreferences(SP_NAME,
Context.MODE_PRIVATE);
String ver = sp.getString(SP_VERSION, null);
//这里首先获取本地补丁包的版本和传入的版本号做对比
if (ver == null || !ver.equalsIgnoreCase(appVersion)) {
//如果版本不同,说明本地的版本过久则要删除存入新的补丁包版本
cleanPatch();
sp.edit().putString(SP_VERSION, appVersion).commit();
} else {
//相同说明本地有本地补丁包了,则初始化
initPatchs();
}
}
我们看到这里如果本地已经有补丁包且版本未更新,则初始化,我们来看这个initPatchs
方法:
private void initPatchs() {
File[] files = mPatchDir.listFiles();
for (File file : files) {
addPatch(file);
}
}
这个方法很简单,就是将补丁路径下的所有补丁文件调用addPatch
方法添加进去,我们来看看addPatch
方法:
private Patch addPatch(File file) {
Patch patch = null;
if (file.getName().endsWith(SUFFIX)) {
try {
patch = new Patch(file);
mPatchs.add(patch);
} catch (IOException e) {
Log.e(TAG, "addPatch", e);
}
}
return patch;
}
我们看到这个方法主要就是判断文件是不是以.patch
结尾,如果是的话就初始化一个Patch
实例,这个是添加补丁包的关键,我们跟进构造函数看下:
public Patch(File file) throws IOException {
mFile = file;
init();
}
@SuppressWarnings("deprecation")
private void init() throws IOException {
JarFile jarFile = null;
InputStream inputStream = null;
try {
//使用JarFile读取Patch文件
jarFile = new JarFile(mFile);
//获取META-INF/PATCH.MF文件
JarEntry entry = jarFile.getJarEntry(ENTRY_NAME);
inputStream = jarFile.getInputStream(entry);
Manifest manifest = new Manifest(inputStream);
Attributes main = manifest.getMainAttributes();
//因为PATCH.MF里面是以键值对的形式存储的,这里是获取键Patch-Name对应的值
mName = main.getValue(PATCH_NAME);
//这里是获取创建的时间
mTime = new Date(main.getValue(CREATED_TIME));
mClassesMap = new HashMap<String, List<String>>();
Attributes.Name attrName;
String name;
List<String> strings;
for (Iterator<?> it = main.keySet().iterator(); it.hasNext();) {
attrName = (Attributes.Name) it.next();
name = attrName.toString();
//判断name的后缀是否是-Classes,并把name对应的值加入到集合中,对应的值就是class类名的列表
if (name.endsWith(CLASSES)) {
strings = Arrays.asList(main.getValue(attrName).split(","));
if (name.equalsIgnoreCase(PATCH_CLASSES)) {
mClassesMap.put(mName, strings);
} else {
// //为了移除掉"-Classes"的后缀
mClassesMap.put(
name.trim().substring(0, name.length() - 8),// remove
// "-Classes"
strings);
}
}
}
} finally {
if (jarFile != null) {
jarFile.close();
}
if (inputStream != null) {
inputStream.close();
}
}
}
这个方法的逻辑其实就是读取补丁.patch
文件,每个修复包apatch文件其实都是一个jarFile
文件,然后获得其中META-INF/PATCH.MF
文件,PATCH.MF文件中都是键值对的形式,获取key是-Classes
的所有的value,这些value就是所有要修复的类,他们是以“,”进行分割的,将它们放入list列表,将其存储到一个集合中mClassesMap
,list列表中存储的就是所有要修复的类名。我们看到我们还有一个addPatch
方法:
public void addPatch(String path) throws IOException {
File src = new File(path);
File dest = new File(mPatchDir, src.getName());
if(!src.exists()){
throw new FileNotFoundException(path);
}
if (dest.exists()) {
Log.d(TAG, "patch [" + path + "] has be loaded.");
return;
}
FileUtil.copyFile(src, dest);// copy to patch's directory
Patch patch = addPatch(dest);
if (patch != null) {
loadPatch(patch);
}
}
这个方法里面就多了一步将指定的路径下的文件拷贝到补丁包路径下面,这个方法的作用是什么呢?我们知道,我们第一次app下载下来的时候,我们本地是没有补丁包的,只有当需要补丁修复的时候,我们才会从服务器下载补丁包下来,这时候我们可以指定我们下载下来的补丁包的存放路径,然后将它拷贝到补丁包路径,然后进行加载。
接下来,我们来看看方法调用loadPatch()
方法:
public void loadPatch() {
mLoaders.put("*", mContext.getClassLoader());// wildcard
Set<String> patchNames;
List<String> classes;
//遍历mPatchs中的所有Patch
for (Patch patch : mPatchs) {
//遍历补丁包中待修复的所有patch名
patchNames = patch.getPatchNames();
for (String patchName : patchNames) {
//获取patch中的所有class
classes = patch.getClasses(patchName);
//修复bug
mAndFixManager.fix(patch.getFile(), mContext.getClassLoader(),
classes);
}
}
}
我们看到这个方法主要就是最后的fix
方法:
public synchronized void fix(String patchPath) {
fix(new File(patchPath), mContext.getClassLoader(), null);
}
public synchronized void fix(File file, ClassLoader classLoader,
List<String> classes) {
if (!mSupport) {
return;
}
//检测patch包的签名,检查是不是安全的
if (!mSecurityChecker.verifyApk(file)) {// security check fail
return;
}
try {
//判断优化后的文件是否存在,如果存在则检测optfile的安全性
File optfile = new File(mOptDir, file.getName());
boolean saveFingerprint = true;
if (optfile.exists()) {
// need to verify fingerprint when the optimize file exist,
// prevent someone attack on jailbreak device with
// Vulnerability-Parasyte.
// btw:exaggerated android Vulnerability-Parasyte
// http://secauo.com/Exaggerated-Android-Vulnerability-Parasyte.html
if (mSecurityChecker.verifyOpt(optfile)) {
saveFingerprint = false;
} else if (!optfile.delete()) {
return;
}
}
//加载补丁文件包中的dex文件
final DexFile dexFile = DexFile.loadDex(file.getAbsolutePath(),
optfile.getAbsolutePath(), Context.MODE_PRIVATE);
if (saveFingerprint) {
mSecurityChecker.saveOptSig(optfile);
}
//这里重写了ClassLoader中的findClass方法,并且实例化它
ClassLoader patchClassLoader = new ClassLoader(classLoader) {
@Override
protected Class<?> findClass(String className)
throws ClassNotFoundException {
Class<?> clazz = dexFile.loadClass(className, this);
if (clazz == null
&& className.startsWith("com.alipay.euler.andfix")) {
return Class.forName(className);// annotation’s class
// not found
}
if (clazz == null) {
throw new ClassNotFoundException(className);
}
return clazz;
}
};
//遍历dex然后加载有bug的类文件
Enumeration<String> entrys = dexFile.entries();
Class<?> clazz = null;
while (entrys.hasMoreElements()) {
String entry = entrys.nextElement();
if (classes != null && !classes.contains(entry)) {
continue;// skip, not need fix
}
clazz = dexFile.loadClass(entry, patchClassLoader);
if (clazz != null) {
//对有bug的文件进行替换
fixClass(clazz, classLoader);
}
}
} catch (IOException e) {
Log.e(TAG, "pacth", e);
}
}
我们看见我们为什么要用DexFile来加载补丁文件呢?这个得从补丁包.patch
文件来看:
我们看到我们的.patch文件实质上里面包含了两个文件,其中一个
.dex
就是我们要修复的类,META-INF文件如下:META-INF
这个文件就是我们前面说的PATCH.MF,里面存放补丁包的一些配置信息。上面的方法就是加载dex,然后获取到所有要修复的类,然后进行替换,所以我们接下来看fixClass()
方法:
private void fixClass(Class<?> clazz, ClassLoader classLoader) {
Method[] methods = clazz.getDeclaredMethods();
MethodReplace methodReplace;
String clz;
String meth;
//遍历要修复类中的所有方法
for (Method method : methods) {
//获取带有MethodReplace注解的方法
methodReplace = method.getAnnotation(MethodReplace.class);
if (methodReplace == null)
continue;
clz = methodReplace.clazz();
meth = methodReplace.method();
//获取注解中的class信息和method信息
if (!isEmpty(clz) && !isEmpty(meth)) {
//然后调用replaceMethod方法进行替换
replaceMethod(classLoader, clz, meth, method);
}
}
}
我们看到上面方法会获取带有MethodReplace注解的所有方法,这是在补丁包生成的时候添加上去的,然后获取class和method的信息之后,通过调用方法repalceMethod
方法进行方法替换:
private void replaceMethod(ClassLoader classLoader, String clz,
String meth, Method method) {
try {
String key = clz + "@" + classLoader.toString();
//判断这个类是否已经被修复了
Class<?> clazz = mFixedClass.get(key);
//如果class还没有被类加载加载进来,则去加载
if (clazz == null) {// class not load
Class<?> clzz = classLoader.loadClass(clz);
// initialize target class
clazz = AndFix.initTargetClass(clzz);
}
if (clazz != null) {// initialize class OK
mFixedClass.put(key, clazz);
//根据反射获取到有bug的类的方法
Method src = clazz.getDeclaredMethod(meth,
method.getParameterTypes());
//src是有bug的方法,method是补丁方法
AndFix.addReplaceMethod(src, method);
}
} catch (Exception e) {
Log.e(TAG, "replaceMethod", e);
}
}
我们看到通过注解中的method信息获取到有bug的方法,然后调用AndFix.addReplaceMethod(src, method)
进行替换,这里是native来实现的,我们来看看:
static void replaceMethod(JNIEnv* env, jclass clazz, jobject src,
jobject dest) {
if (isArt) {
art_replaceMethod(env, src, dest);
} else {
dalvik_replaceMethod(env, src, dest);
}
}
我们的看到dalvik和art虚拟机的替换方法不同,我们先来看看dalvik虚拟机上面的:
extern void __attribute__ ((visibility ("hidden"))) dalvik_replaceMethod(
JNIEnv* env, jobject src, jobject dest) {
//这里通过jni获取jobject对象
jobject clazz = env->CallObjectMethod(dest, jClassMethod);
//我们看到刚才Hook的系统的函数,通过这个系统函数我们可以获取class的ClassObject对象指针
ClassObject* clz = (ClassObject*) dvmDecodeIndirectRef_fnPtr(
dvmThreadSelf_fnPtr(), clazz);
//更改状态为类初始化完成的状态
clz->status = CLASS_INITIALIZED;
//通过java层传递的方法对象,在native层获得他们的结构体
Method* meth = (Method*) env->FromReflectedMethod(src);
Method* target = (Method*) env->FromReflectedMethod(dest);
LOGD("dalvikMethod: %s", meth->name);
// meth->clazz = target->clazz;
替换method结构体中的accessFlags
meth->accessFlags |= ACC_PUBLIC;
//替换method结构体中的方法的索引methodIndex
meth->methodIndex = target->methodIndex;
// 替换method结构体的缓存的JNI参数jniArgInfo
meth->jniArgInfo = target->jniArgInfo;
meth->registersSize = target->registersSize;
meth->outsSize = target->outsSize;
//替换method结构体的insSize
meth->insSize = target->insSize;
// 替换method结构体的方法原型描述prototype
meth->prototype = target->prototype;
// 替换method结构体的真实代码insns
meth->insns = target->insns;
//替换method结构体的真实函数或者JNI桥接函数
meth->nativeFunc = target->nativeFunc;
}
我们看到这里就是指针指向了修复完的方法的结构体信息,这样就达到了修复的目的,所以说指针还是一个非常有用的东西,这也是c和c++的精髓所在,接着我们来看看art虚拟机的replaceMethod
方法:
extern void __attribute__ ((visibility ("hidden"))) art_replaceMethod(
JNIEnv* env, jobject src, jobject dest) {
if (apilevel > 23) {
replace_7_0(env, src, dest);
} else if (apilevel > 22) {
replace_6_0(env, src, dest);
} else if (apilevel > 21) {
replace_5_1(env, src, dest);
} else if (apilevel > 19) {
replace_5_0(env, src, dest);
}else{
replace_4_4(env, src, dest);
}
}
这个地方还是比较蛋疼的,我们看见根据api的不同版本来做了好几个适配,现在8.0出来了,可想而知也可能要做适配,我们先来看下4.4的这个方法:
void replace_4_4(JNIEnv* env, jobject src, jobject dest) {
art::mirror::ArtMethod* smeth =
(art::mirror::ArtMethod*) env->FromReflectedMethod(src);
art::mirror::ArtMethod* dmeth =
(art::mirror::ArtMethod*) env->FromReflectedMethod(dest);
//替换 ArtMethod的class_loader指针
dmeth->declaring_class_->class_loader_ =
smeth->declaring_class_->class_loader_; //for plugin classloader
//替换ArtMethod的clinit_thread_id_指针
dmeth->declaring_class_->clinit_thread_id_ =
smeth->declaring_class_->clinit_thread_id_;
//替换ArtMethod的status_指针
dmeth->declaring_class_->status_ = smeth->declaring_class_->status_-1;
//for reflection invoke
reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->super_class_ = 0;
//替换ArtMethod的declaring_class_指针
smeth->declaring_class_ = dmeth->declaring_class_;
smeth->dex_cache_initialized_static_storage_ = dmeth->dex_cache_initialized_static_storage_;
//替换ArtMethod的access_flags_指针
smeth->access_flags_ = dmeth->access_flags_ | 0x0001;
smeth->dex_cache_resolved_types_ = dmeth->dex_cache_resolved_types_;
smeth->dex_cache_resolved_methods_ = dmeth->dex_cache_resolved_methods_;
smeth->dex_cache_strings_ = dmeth->dex_cache_strings_;
smeth->code_item_offset_ = dmeth->code_item_offset_;
smeth->core_spill_mask_ = dmeth->core_spill_mask_;
smeth->fp_spill_mask_ = dmeth->fp_spill_mask_;
smeth->method_dex_index_ = dmeth->method_dex_index_;
smeth->mapping_table_ = dmeth->mapping_table_;
//替换ArtMethod的method_index_指针
smeth->method_index_ = dmeth->method_index_;
smeth->gc_map_ = dmeth->gc_map_;
smeth->frame_size_in_bytes_ = dmeth->frame_size_in_bytes_;
smeth->native_method_ = dmeth->native_method_;
smeth->vmap_table_ = dmeth->vmap_table_;
smeth->entry_point_from_compiled_code_ = dmeth->entry_point_from_compiled_code_;
smeth->entry_point_from_interpreter_ = dmeth->entry_point_from_interpreter_;
smeth->method_index_ = dmeth->method_index_;
LOGD("replace_4_4: %d , %d", smeth->entry_point_from_compiled_code_,
dmeth->entry_point_from_compiled_code_);
}
我们看到这里要替换的东西还是比较多的,大家可以通过查看虚拟机中的方法结构体来查看,这里也就不详细列出了。
总结:AndFix的源码还是比较好理解,但是想到用这种方法还是非常fashion的有没有,而且需要我们了解你虚拟机相关的知识,这方面我也是有欠缺,可以适当补充了,希望大家一起进步。
网友评论