写在前面
Github地址镇楼:AndFix
首先介绍一下AndFix及其使用方法,然后根据使用流程对其内部实现进行分析。
AndFix介绍
首先我们看一下不同热修复框架的功能特性对比:
特性 | AndFix | Tinker/Amigo | QQ空间 | Robust/Aceso |
---|---|---|---|---|
即时生效 | 是 | 否 | 否 | 是 |
方法替换 | 是 | 是 | 是 | 是 |
类替换 | 否 | 是 | 是 | 否 |
类结构修改 | 否 | 是 | 否 | 否 |
资源替换 | 否 | 是 | 是 | 否 |
so替换 | 否 | 是 | 否 | 否 |
支持gradle | 否 | 是 | 否 | 否 |
支持ART | 是 | 是 | 是 | 是 |
支持Android7.0 | 是 | 是 | 是 | 是 |
AndFix的优势在于可以即时生效、接入和使用十分简单,但是缺点是只支持方法的替换,其他的替换不支持,且不支持gradle。Github上给出了它的适用范围:
AndFix supports Android version from 2.3 to 7.0, both ARM and X86 architecture, both Dalvik and ART runtime, both 32bit and 64bit.
AndFix支持Android版本从2.3到7.0,ARM和X86体系结构,Dalvik和ART运行时(32位和64位)。
源码中也对此进行了对应的限制:
### Compat.java
// from android 2.3 to android 7.0
private static boolean isSupportSDKVersion() {
if (android.os.Build.VERSION.SDK_INT >= 8
&& android.os.Build.VERSION.SDK_INT <= 24) {
return true;
}
return false;
}
AndFix的修复流程图如下:
前面几步就是发现bug、定位bug、修改、测试等一系列操作,不涉及到SDK中的内容。当修复了bug,我们打出新的apk之后(后称修改后的为new.apk,有bug的为old.apk),通过apkpatch工具生成patch文件。
apkpatch的使用方法如下:
usage: apkpatch -f -t -o -k -p <***> -a -e <***>
-a,–alias KeyStore.Entry别名
-e,–epassword <***> KeyStore.Entry密码
-f,–from new.apk文件路径
-k,–keystore 签名文件路径
-n,–name patch文件名
-o,–out输出路径
-p,–kpassword <***> keystore密码
-t,–to old.apk文件路径
AndFix使用
1.Initialize PatchManager
patchManager = new PatchManager(context);
patchManager.init(appversion);//current version
2.Load patch
patchManager.loadPatch();
You should load patch as early as possible, generally, in the initialization phase of your application(such as Application.onCreate()).
3.Add patch
patchManager.addPatch(path);//path of the patch file that was downloaded
When a new patch file has been downloaded, it will become effective immediately by addPatch.
上面是Github上官方给出的加载流程,即AndFix提供的是一套生成patch、合并patch、应用patch的流程,在你自己的项目中使用时,还需要你自己去实现一套类似与更新检测下载的流程。
AndFix源码分析
首先给出一个源码的关系图,有一个整体的认识:
源码分析按照官方给出的使用流程的顺序来进行阅读
1.Initialize PatchManager
patchManager = new PatchManager(context);
patchManager.init(appversion);//current version
在这里我们看到了第一个关键类,PatchManager。从上面的使用方法中可以看到,PatchManager提供的入口类,构造函数实现:
public PatchManager(Context context) {
mContext = context;
mAndFixManager = new AndFixManager(mContext);
mPatchDir = new File(mContext.getFilesDir(), DIR);
mPatchs = new ConcurrentSkipListSet<Patch>();
mLoaders = new ConcurrentHashMap<String, ClassLoader>();
}
构造函数中初始化了成员变量:
AndFixManager负责调用fix、replaceMethod等替换方法的关键代码
PatchDir是patch文件的存放路径
Patchs是读取出的所有patch文件
Loaders是指定的ClassLoader,默认使用context.getClassLoader()
接下来看一下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;
}
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();
}
}
init中首先判断apatch文件夹是否存在,然后从SharedPreferences中取出保存的版本号进行对比:如果版本号不存在或是不相同,则调用cleanPatch清理掉所有patch文件并更新版本号;相同情况下才会进行patch是否可以应用的判断initPatchs。我们先来看清理patch的代码:
# PatchManager
private void cleanPatch() {
File[] files = mPatchDir.listFiles();
for (File file : files) {
mAndFixManager.removeOptFile(file);
if (!FileUtil.deleteFile(file)) {
Log.e(TAG, file.getName() + " delete error.");
}
}
}
# AndFixManager
public synchronized void removeOptFile(File file) {
File optfile = new File(mOptDir, file.getName());
if (optfile.exists() && !optfile.delete()) {
Log.e(TAG, optfile.getName() + " delete error.");
}
}
这里在清理apatch文件夹下patch文件的同时,也清理了apatch_opt文件下的文件。apatch_opt文件夹下的文件在后面会讲到,这里简单提一下:首先,AndFix的热修复方案并不是从根本上改变了我们安装程序的字节码,而是在程序运行时,将原来指向old.apk中有bug的方法的指针,指向new.apk中新方法。这也是AndFix有明确的版本大小判断的原因,因为当前只做了对这些版本的虚拟机的方案。其次,关于opt文件夹下生成文件的原因,就是.apatch格式是自定义的格式,在后面的方法中可以看到,第一次执行时需要转换成DexFile才能从中获取到类的各种信息,而这个转换过程也会占用很多的资源,所以直接将转换好的文件存放在一个新的文件夹中,以空间换时间,故清理时也要同步清理干净。
接下来我们看一下初始化patch文件的操作initPatchs:
private static final String SUFFIX = ".apatch";
private void initPatchs() {
File[] files = mPatchDir.listFiles();
for (File file : files) {
addPatch(file);
}
}
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文件,存放在Set中。至此,初始化的操作就完成了。
2.Load patch
patchManager.loadPatch();
# PatchManager
/**
* load patch,call when application start
*/
public void loadPatch() {
mLoaders.put("*", mContext.getClassLoader());// wildcard
Set<String> patchNames;
List<String> classes;
for (Patch patch : mPatchs) {
patchNames = patch.getPatchNames();
for (String patchName : patchNames) {
classes = patch.getClasses(patchName);
mAndFixManager.fix(patch.getFile(), mContext.getClassLoader(),
classes);
}
}
}
# Patch
public Set<String> getPatchNames() {
return mClassesMap.keySet();
}
public List<String> getClasses(String patchName) {
return mClassesMap.get(patchName);
}
loadPatch无参函数对应的是当app启动时调用的,所以ClassLoader使用的是mContext.getClassLoader()。接下来遍历mPatchs,Patch中的mClassesMap保存了需要进行修复的class名,对每个class调用mAndFixManager.fix。我们先来看一下mClassesMap的生成:
private static final String ENTRY_NAME = "META-INF/PATCH.MF";
private static final String CLASSES = "-Classes";
private static final String PATCH_CLASSES = "Patch-Classes";
private static final String CREATED_TIME = "Created-Time";
private static final String PATCH_NAME = "Patch-Name";
public Patch(File file) throws IOException {
mFile = file;
init();
}
@SuppressWarnings("deprecation")
private void init() throws IOException {
JarFile jarFile = null;
InputStream inputStream = null;
try {
jarFile = new JarFile(mFile);
JarEntry entry = jarFile.getJarEntry(ENTRY_NAME);
inputStream = jarFile.getInputStream(entry);
Manifest manifest = new Manifest(inputStream);
Attributes main = manifest.getMainAttributes();
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();
if (name.endsWith(CLASSES)) {
strings = Arrays.asList(main.getValue(attrName).split(","));
if (name.equalsIgnoreCase(PATCH_CLASSES)) {
mClassesMap.put(mName, strings);
} else {
mClassesMap.put(
name.trim().substring(0, name.length() - 8),// remove "-Classes"
strings);
}
}
}
} finally {
if (jarFile != null) {
jarFile.close();
}
if (inputStream != null) {
inputStream.close();
}
}
}
Patch实例化时,将文件按照固定格式进行解析,取出所有结尾为“-Classes”的attrName放入mClassesMap中。接下来我们看一下AndFixManager中的fix方法,由于方法过长,已经将注释加到了代码之中:
public synchronized void fix(File file, ClassLoader classLoader,
List<String> classes) {
// SecurityChecker中检测的版本是否支持
if (!mSupport) {
return;
}
// 检测签名,防止恶意植入
if (!mSecurityChecker.verifyApk(file)) {// security check fail
return;
}
try {
// 检测opt文件夹下对应的文件:如果存在,检测指纹(第一次生成时生成了对应的指纹信息,保存在了sp中)
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;
}
}
// 注1,见后面
final DexFile dexFile = DexFile.loadDex(file.getAbsolutePath(),
optfile.getAbsolutePath(), Context.MODE_PRIVATE);
// 如果不是已存在的文件,则需要保存指纹信息,用于下次使用时校验
if (saveFingerprint) {
mSecurityChecker.saveOptSig(optfile);
}
// 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;
}
};
// 注2,见后面
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
}
// 此时classes中含有该entry,加载这个class
clazz = dexFile.loadClass(entry, patchClassLoader);
if (clazz != null) {
// 调用fixClass修复
fixClass(clazz, classLoader);
}
}
} catch (IOException e) {
Log.e(TAG, "pacth", e);
}
}
// 注1:附上loadDex方法的注释,在loadDex方法中实现了将dex文件持久化及服用的操作
# DexFile
/**
* Open a DEX file, specifying the file in which the optimized DEX
* data should be written. If the optimized form exists and appears
* to be current, it will be used; if not, the VM will attempt to
* regenerate it.
*
* This is intended for use by applications that wish to download
* and execute DEX files outside the usual application installation
* mechanism. This function should not be called directly by an
* application; instead, use a class loader such as
* dalvik.system.DexClassLoader.
*
* @param sourcePathName
* Jar or APK file with "classes.dex". (May expand this to include
* "raw DEX" in the future.)
* @param outputPathName
* File that will hold the optimized form of the DEX data.
* @param flags
* Enable optional features. (Currently none defined.)
* @return
* A new or previously-opened DexFile.
* @throws IOException
* If unable to open the source or output file.
*/
static public DexFile loadDex(String sourcePathName, String outputPathName,
int flags) throws IOException
// 注2
/**
* Enumerate the names of the classes in this DEX file.
*
* @return an enumeration of names of classes contained in the DEX file, in
* the usual internal form (like "java/lang/String").
*/
public Enumeration<String> entries() {
return new DFEnum(this);
}
/*
* Helper class.
*/
private class DFEnum implements Enumeration<String> {
private int mIndex;
private String[] mNameList;
DFEnum(DexFile df) {
mIndex = 0;
mNameList = getClassNameList(mCookie);
}
public boolean hasMoreElements() {
return (mIndex < mNameList.length);
}
public String nextElement() {
return mNameList[mIndex++];
}
}
简单总结一下上面的代码,我们先检测文件的安全性(防止恶意修改),然后根据class名找到要修复的Class对象,调用fixClass方法。接下来我们来看fixClass方法:
private void fixClass(Class<?> clazz, ClassLoader classLoader) {
// 获取到所有方法
Method[] methods = clazz.getDeclaredMethods();
MethodReplace methodReplace;
String clz;
String meth;
for (Method method : methods) {
// 在生成patch文件时,在对应方法上添加了注解{@MethodReplace}
methodReplace = method.getAnnotation(MethodReplace.class);
if (methodReplace == null)
continue;
clz = methodReplace.clazz();
meth = methodReplace.method();
if (!isEmpty(clz) && !isEmpty(meth)) {
// 找到了需要替换的方法,进行替换
replaceMethod(classLoader, clz, meth, method);
}
}
}
private void replaceMethod(ClassLoader classLoader, String clz,
String meth, Method method) {
try {
String key = clz + "@" + classLoader.toString();
Class<?> clazz = mFixedClass.get(key);
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);
Method src = clazz.getDeclaredMethod(meth,
method.getParameterTypes());
AndFix.addReplaceMethod(src, method);
}
} catch (Exception e) {
Log.e(TAG, "replaceMethod", e);
}
}
前面部分增加了注释不多说了,后面部分其实就是调用了AndFix中的initTargetClass和addReplaceMethod方法。我们先来看initTargetClass,该方法的目的时将方法域修改为public:
public static Class<?> initTargetClass(Class<?> clazz) {
try {
Class<?> targetClazz = Class.forName(clazz.getName(), true,
clazz.getClassLoader());
// 核心代码
initFields(targetClazz);
return targetClazz;
} catch (Exception e) {
Log.e(TAG, "initTargetClass", e);
}
return null;
}
private static void initFields(Class<?> clazz) {
Field[] srcFields = clazz.getDeclaredFields();
for (Field srcField : srcFields) {
Log.d(TAG, "modify " + clazz.getName() + "." + srcField.getName()
+ " flag:");
setFieldFlag(srcField);
}
}
private static native void setFieldFlag(Field field);
经过一步步的调用,最终调用了这个native方法,我们进去看一下:
static void setFieldFlag(JNIEnv* env, jclass clazz, jobject field) {
if (isArt) {
art_setFieldFlag(env, field);
} else {
dalvik_setFieldFlag(env, field);
}
}
根据虚拟机不同,调用不同方法,我们来看一下art的
extern void __attribute__ ((visibility ("hidden"))) art_setFieldFlag(
JNIEnv* env, jobject field) {
if (apilevel > 23) {
setFieldFlag_7_0(env, field);
} else if (apilevel > 22) {
setFieldFlag_6_0(env, field);
} else if (apilevel > 21) {
setFieldFlag_5_1(env, field);
} else if (apilevel > 19) {
setFieldFlag_5_0(env, field);
}else{
setFieldFlag_4_4(env, field);
}
}
// 以4.4为例
void setFieldFlag_4_4(JNIEnv* env, jobject field) {
art::mirror::ArtField* artField =
(art::mirror::ArtField*) env->FromReflectedField(field);
artField->access_flags_ = artField->access_flags_ & (~0x0002) | 0x0001;
LOGD("setFieldFlag_4_4: %d ", artField->access_flags_);
}
至此就将方法域修改为了public。接下来我们看一下addReplaceMethod方法:
public static void addReplaceMethod(Method src, Method dest) {
try {
replaceMethod(src, dest);
initFields(dest.getDeclaringClass());
} catch (Throwable e) {
Log.e(TAG, "addReplaceMethod", e);
}
}
private static native void replaceMethod(Method dest, Method src);
到这个native方法中看一看:
// 我们还是以art为例
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"))) 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);
}
}
void replace_4_4(JNIEnv* env, jobject src, jobject dest) {
// old.apk中对应的方法
art::mirror::ArtMethod* smeth =
(art::mirror::ArtMethod*) env->FromReflectedMethod(src);
// new.apk中对应的方法
art::mirror::ArtMethod* dmeth =
(art::mirror::ArtMethod*) env->FromReflectedMethod(dest);
dmeth->declaring_class_->class_loader_ =
smeth->declaring_class_->class_loader_; //for plugin classloader
dmeth->declaring_class_->clinit_thread_id_ =
smeth->declaring_class_->clinit_thread_id_;
dmeth->declaring_class_->status_ = smeth->declaring_class_->status_-1;
//for reflection invoke
reinterpret_cast<art::mirror::Class*>(dmeth->declaring_class_)->super_class_ = 0;
// 指针指向新方法
smeth->declaring_class_ = dmeth->declaring_class_;
smeth->dex_cache_initialized_static_storage_ = dmeth->dex_cache_initialized_static_storage_;
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_;
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_);
}
最后这部分代码很长,可以看出是指针指向新方法,具体的变量我不是很懂,留给大佬们去解释吧~至此,loadPatch的流程就结束了。
3.Add patch
patchManager.addPatch(path);//path of the patch file that was downloaded
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);
}
}
private void loadPatch(Patch patch) {
Set<String> patchNames = patch.getPatchNames();
ClassLoader cl;
List<String> classes;
for (String patchName : patchNames) {
if (mLoaders.containsKey("*")) {
cl = mContext.getClassLoader();
} else {
cl = mLoaders.get(patchName);
}
if (cl != null) {
classes = patch.getClasses(patchName);
mAndFixManager.fix(patch.getFile(), cl, classes);
}
}
}
这里可以看到,addPatch最后调用了loadPatch方法,流程与上一步一样,这里就不多解释了。
至此,AndFix的源码分析就结束了,如果出现了错误,麻烦大佬们指出~
结语
If you like it, it’s written by Johnny Deng.
If not,then I don’t know who wrote it.