拆轮子之热修复框架AndFix
2016-09-27 14:38
295 查看
这一两年各种热修复框架风起云涌,各种优秀开源框架不断推陈出新,今天就来介绍一下AndFix,虽然这套框架不是能解决所有问题,但其中的思想精髓还是很值得研究一下的。
这里我们要对源码进行分析,因此使用导入源码作为library的方式
2、在Application里面初始化
mPatchmanager.init(”1.0”)这里1.0是appVersion
注意每次appversion变更都会导致所有补丁被删除,如果appversion没有改变,则会加载已经保存的所有补丁。
3、代码部分就基本完成了,剩余的是制作apatch,首先生成一个apk文件,然后更改代码,在修复bug后生成另一个apk。生成一个.apatch格式的补丁文件,需要提供原apk,修复后的apk,以及一个签名文件。
可以直接使用命令apkpatch查看具体的使用方法。
使用示例:
最后会得到一个apatch后缀文件,该文件即为修复后的补丁包,下载到手机里(具体目录看代码里设置的加载补丁包的位置),即可完成修复。
这里面也初始化了一个AndFixManager
其中有个检验设备是否支持AndFix的方法
上面这个方法可以看出AndFix不支持阿里的云os操作系统,并且只支持2.3到6.0的系统,同时即使是支持的系统也有一定的兼容性问题,兼容性问题放在AndFix.setup()里处理,里面在调用native方进行判断。
每次我们升级了apk就应当相应地修改传入init方法的appVersion,一旦检测到版本不一样,就会cleanPatch()
直接删除了app目录下的apatch和aptch_opt目录下的文件,因为发布新版本,意味着我们肯定将之前的线上bug做了修复,因此之前的修复apatch就不需要加载。
反之如果不是app升级,依然会通过initPatchs();把apatch包加载进来
每次我们下载了修复包都会被复制到app自身目录的apatch目录下,因此在初始化的时候就会变量该目录下的文件,将每个apatch文件add进来,都统一存放在mPatchs里面
重点在init方法里,通过AndFix提供的工具打出的apatch包,我们可以来看看目录结构
里面有个classes.dex,聪明的你可能想到了里面应该放着修改过的类,用反汇编工具处理下然后打开看下,显然刚刚猜想的答案是正确的
而且修改过的方法都被加了@MethodReplace 里面有类名和方法名两个数据,显然之后我们会用到这两个数据
apatch还有个META-INF目录,打开看里面重点看PATCH.MF
然后我们回去过看Patch的init方法,就比较容易看懂它做了什么,获取名称,创建apatch包时间,以及解析Patch-Classes,把有修改的类放到List数组里存放在mClassMap中。
重点在mAndFixManager的fix方法
刚开始会做一些签名验证来判断apatch文件的合法性。然后就是修复bug了。通过DexFile.loadDex得到apatch里面的classes.dex,然后遍历里面的类和方法,如果这个类和前面说到的PATCH.MF里面的PATCH-CLASSES有一样,证明这是需要修改的类,马上通过loadClass加载出实例,进入fixClass做修复
在前面的分析中我们知道需要修改的方法都被加上注解,这里通过获取注解得到需要修改的类和方法,并
利用反射获取Class实例和方法实例,加上传入替换方法实例,我们就可以传入到native层在native层实现方法替换,native层根据是dalvik还是art模式有不同的处理方法,这里不做深入分析。
分析完loadPatch方法,后续根据下载下来的apatch去addPatch整个逻辑也大体上差不多。
整个方案的思路还是比较明确的,甚至可以不需要重启就可以打补丁,但是局限性也比较大,只能修改方法来修复bug,我在自己做的项目上实验了下,有些bug想要只修改方法来完成貌似有点做不到,这样就大大局限了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里面获取一个Patchmanagerpublic 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的使用,但作为一种学习热修复的知识还是一个挺好的框架的。
相关文章推荐
- 从源码安装Mysql/Percona 5.5
- 浅析Ruby的源代码布局及其编程风格
- asp.net 抓取网页源码三种实现方法
- JS小游戏之仙剑翻牌源码详解
- JS小游戏之宇宙战机源码详解
- 深入浅析knockout源码分析之订阅
- jQuery源码分析之jQuery中的循环技巧详解
- 本人自用的global.js库源码分享
- java中原码、反码与补码的问题分析
- ASP.NET使用HttpWebRequest读取远程网页源代码
- PHP网页游戏学习之Xnova(ogame)源码解读(六)
- C#获取网页HTML源码实例
- PHP网页游戏学习之Xnova(ogame)源码解读(八)
- PHP网页游戏学习之Xnova(ogame)源码解读(四)
- 深入理解PHP之源码目录结构与功能说明
- JS小游戏之极速快跑源码详解
- JS小游戏之象棋暗棋源码详解
- android源码探索之定制android关机界面的方法
- 基于Android设计模式之--SDK源码之策略模式的详解
- Android游戏源码分享之2048