拆轮子之热修复框架AndFix

个人博客 迁移,欢迎光临

这一两年各种热修复框架风起云涌,各种优秀开源框架不断推陈出新,今天就来介绍一下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包,我们可以来看看目录结构
apatch文件结构
里面有个classes.dex,聪明的你可能想到了里面应该放着修改过的类,用反汇编工具处理下然后打开看下,显然刚刚猜想的答案是正确的
这里写图片描述
而且修改过的方法都被加了@MethodReplace 里面有类名和方法名两个数据,显然之后我们会用到这两个数据
apatch还有个META-INF目录,打开看里面重点看PATCH.MF
这里写图片描述
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的使用,但作为一种学习热修复的知识还是一个挺好的框架的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值