AndFix源码分析

写在前面

Github地址镇楼:AndFix

首先介绍一下AndFix及其使用方法,然后根据使用流程对其内部实现进行分析。


AndFix介绍

首先我们看一下不同热修复框架的功能特性对比:

特性AndFixTinker/AmigoQQ空间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,--aliasKeyStore.Entry别名
-e,--epassword <***>   KeyStore.Entry密码
-f,--fromnew.apk文件路径
-k,--keystore签名文件路径
-n,--namepatch文件名
-o,--out

输出路径
-p,--kpassword <***>   keystore密码
-t,--toold.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.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值