您的位置:首页 > 其它

拆轮子之热修复框架AndFix

2016-09-27 14:38 295 查看
这一两年各种热修复框架风起云涌,各种优秀开源框架不断推陈出新,今天就来介绍一下AndFix,虽然这套框架不是能解决所有问题,但其中的思想精髓还是很值得研究一下的。

使用方法

1、从AndFix 官网下载最新的AndFix代码,导入到Demo工程里作为library,也可以用添加依赖的方式

compile 'com.alipay.euler:andfix:0.3.1@aar'


这里我们要对源码进行分析,因此使用导入源码作为library的方式

2、在Application里面初始化

// initialize
mPatchManager = new PatchManager(this);
mPatchManager.init("1.0");
Log.d(TAG, "inited.");

// load patch
mPatchManager.loadPatch();
//        Log.d(TAG, "apatch loaded.");

// add patch at runtime
try {
// .apatch file path
String patchFileString = Environment.getExternalStorageDirectory()
.getAbsolutePath() + APATCH_PATH;
mPatchManager.addPatch(patchFileString);
Log.d(TAG, "apatch:" + patchFileString + " added.");

//复制且加载补丁成功后,删除下载的补丁
File f = new File(this.getFilesDir(), DIR + APATCH_PATH);
if (f.exists()) {
boolean result = new File(patchFileString).delete();
if (!result)
Log.e(TAG, patchFileString + " delete fail");
}
} catch (IOException e) {
Log.e(TAG, "", e);
}


mPatchmanager.init(”1.0”)这里1.0是appVersion

注意每次appversion变更都会导致所有补丁被删除,如果appversion没有改变,则会加载已经保存的所有补丁。

3、代码部分就基本完成了,剩余的是制作apatch,首先生成一个apk文件,然后更改代码,在修复bug后生成另一个apk。生成一个.apatch格式的补丁文件,需要提供原apk,修复后的apk,以及一个签名文件。

可以直接使用命令apkpatch查看具体的使用方法。

使用示例:

apkpatch -o D:/Patch/ -k debug.keystore -p android-a androiddebugkey -e android f bug-fix.apk t release.apk


最后会得到一个apatch后缀文件,该文件即为修复后的补丁包,下载到手机里(具体目录看代码里设置的加载补丁包的位置),即可完成修复。

原理分析

初始化

在Application里面获取一个Patchmanager

public PatchManager(Context context) {
mContext = context;
mAndFixManager = new AndFixManager(mContext);
mPatchDir = new File(mContext.getFilesDir(), DIR); //获取存放apatch目录
mPatchs = new ConcurrentSkipListSet<Patch>();
mLoaders = new ConcurrentHashMap<String, ClassLoader>();
}


这里面也初始化了一个AndFixManager

public AndFixManager(Context context) {
mContext = context;
mSupport = Compat.isSupport(); //检验是否支持
if (mSupport) {
mSecurityChecker = new SecurityChecker(mContext); //用于检验包的签名等信息
mOptDir = new File(mContext.getFilesDir(), DIR); //apatch优化包存放路径
if (!mOptDir.exists() && !mOptDir.mkdirs()) {// make directory fail
mSupport = false;
Log.e(TAG, "opt dir create error.");
} else if (!mOptDir.isDirectory()) {// not directory
mOptDir.delete();
mSupport = false;
}
}
}


其中有个检验设备是否支持AndFix的方法

public static synchronized boolean isSupport() {
if (isChecked)
return isSupport;

isChecked = true;
// not support alibaba's YunOs
if (!isYunOS() && AndFix.setup() && isSupportSDKVersion()) {
isSupport = true;
}

if (inBlackList()) {
isSupport = false;
}

return isSupport;
}


上面这个方法可以看出AndFix不支持阿里的云os操作系统,并且只支持2.3到6.0的系统,同时即使是支持的系统也有一定的兼容性问题,兼容性问题放在AndFix.setup()里处理,里面在调用native方进行判断。

修复包版本控制

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();
}
}


每次我们升级了apk就应当相应地修改传入init方法的appVersion,一旦检测到版本不一样,就会cleanPatch()

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.");
}
}
}

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.");
}
}


直接删除了app目录下的apatch和aptch_opt目录下的文件,因为发布新版本,意味着我们肯定将之前的线上bug做了修复,因此之前的修复apatch就不需要加载。

反之如果不是app升级,依然会通过initPatchs();把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;
}


每次我们下载了修复包都会被复制到app自身目录的apatch目录下,因此在初始化的时候就会变量该目录下的文件,将每个apatch文件add进来,都统一存放在mPatchs里面

Patch实例

通过前面我们可以知道一个Patch类代表着我们一个apatch包,具体看看Patch是怎么处理apatch的

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();
}
}

}


重点在init方法里,通过AndFix提供的工具打出的apatch包,我们可以来看看目录结构



里面有个classes.dex,聪明的你可能想到了里面应该放着修改过的类,用反汇编工具处理下然后打开看下,显然刚刚猜想的答案是正确的



而且修改过的方法都被加了@MethodReplace 里面有类名和方法名两个数据,显然之后我们会用到这两个数据

apatch还有个META-INF目录,打开看里面重点看PATCH.MF





然后我们回去过看Patch的init方法,就比较容易看懂它做了什么,获取名称,创建apatch包时间,以及解析Patch-Classes,把有修改的类放到List数组里存放在mClassMap中。

加载修复包

根据前面的分析,我们把所需要修复的bug的一些信息基本都加载进入内存了,现在要做的就是去修复它,因此我们再来看看mPatchManager.loadPatch(),这个方法就是真正的处理加载热修复包的逻辑。

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);
}
}
}


重点在mAndFixManager的fix方法

public synchronized void fix(File file, ClassLoader classLoader,
List<String> classes) {
if (!mSupport) {
return;
}

if (!mSecurityChecker.verifyApk(file)) {// security check fail
return;
}

try {
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;
}
}

final DexFile dexFile = DexFile.loadDex(file.getAbsolutePath(),
optfile.getAbsolutePath(), Context.MODE_PRIVATE);

if (saveFingerprint) {
mSecurityChecker.saveOptSig(optfile);
}

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;
}
};
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) {
fixClass(clazz, classLoader);
}
}
} catch (IOException e) {
Log.e(TAG, "pacth", e);
}
}


刚开始会做一些签名验证来判断apatch文件的合法性。然后就是修复bug了。通过DexFile.loadDex得到apatch里面的classes.dex,然后遍历里面的类和方法,如果这个类和前面说到的PATCH.MF里面的PATCH-CLASSES有一样,证明这是需要修改的类,马上通过loadClass加载出实例,进入fixClass做修复

private void fixClass(Class<?> clazz, ClassLoader classLoader) {
Method[] methods = clazz.getDeclaredMethods();
MethodReplace methodReplace;
String clz;
String meth;
for (Method method : methods) {
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);
}
}


利用反射获取Class实例和方法实例,加上传入替换方法实例,我们就可以传入到native层在native层实现方法替换,native层根据是dalvik还是art模式有不同的处理方法,这里不做深入分析。

分析完loadPatch方法,后续根据下载下来的apatch去addPatch整个逻辑也大体上差不多。

整体思路

分析完上面的逻辑,我们可以看出来AndFix核心在于在native层做方法替换,就像官网的图例一样



整个方案的思路还是比较明确的,甚至可以不需要重启就可以打补丁,但是局限性也比较大,只能修改方法来修复bug,我在自己做的项目上实验了下,有些bug想要只修改方法来完成貌似有点做不到,这样就大大局限了AndFix的使用,但作为一种学习热修复的知识还是一个挺好的框架的。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  源码