AndFix工作原理

AndFix是Alibaba开源的热修复框架之一,还有一个是Dexposed。AndFix和Dexposed都是基于Native Hook的原理。

在开始分析AndFix之前,我们先看看Android的Native和Java世界之间的交互。

如果Java代码需要调用Native的代码,我们需要做三件事情:

  1. 在java代码里面用native关键字声明需要调用的Native世界的函数。
  2. 在Native世界定义实现Native函数。
  3. 向JVM注册native函数。注册的方式有两种:静态注册和动态注册。

静态注册Native函数就是按照约定来给native函数命名。基本上就是把包名中的'.'替换成'_',来个native函数取名字。动态注册的需要在native函数真正被调用之前,提供给JVM一个函数映射表。函数映射表指明了Java层面需要的native函数和Native层面提供的函数对应关系。

如果Native的代码需要调用Java的代码,需要

  1. 通过JNIEnv获取Class
  2. 获取jmethodID
  3. 调用Java方法

现在来看AndFix的使用方法。

第一是初始化PatchManager。

patchManager = new PatchManager(context);
patchManager.init(appversion);//current version

PatchManager构造函数中创建了AndFixManager。AndFixManager调用Compat.isSupport()来确认当前的环境是不是支持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;
	}

阿里的YunOS首先是不支持的。支持的SDK版本从android 2.3到android 6.0,支持度还是很高。AndFix.setup()内部做了不少事情。AndFix.setup()会根据当前使用的Dalvik还是ART虚拟机做不同的初始化工作。分别对应的是dalvik_setup和art_setup。

art_setup很简单,只是记录了apilevel。以便后面替换方法的时候选取不同的处理函数。

dalvik_setup会加载libdvm.so,获取两个函数的指针:dvmDecodeIndirectRef和dvmThreadSelf。每一个java的类在虚拟机的实现中都对应着一个C++的ClassObject。dvmDecodeIndirectRef是libdvm中的方法,它可以从java对象的间接引用获得ClassObject对象,再根据slot,用dvmSlotToMethod找到Method对象。这里的ClassObject和Method都是虚拟机内部用来表示class和Method的数据结构。

extern jboolean __attribute__ ((visibility ("hidden"))) dalvik_setup(
		JNIEnv* env, int apilevel) { void* dvm_hand = dlopen("libdvm.so", RTLD_NOW); if (dvm_hand) {
		dvmDecodeIndirectRef_fnPtr = dvm_dlsym(dvm_hand,
				apilevel > 10 ? "_Z20dvmDecodeIndirectRefP6ThreadP8_jobject" : "dvmDecodeIndirectRef"); if (!dvmDecodeIndirectRef_fnPtr) { return JNI_FALSE;
		}
		dvmThreadSelf_fnPtr = dvm_dlsym(dvm_hand,
				apilevel > 10 ? "_Z13dvmThreadSelfv" : "dvmThreadSelf"); if (!dvmThreadSelf_fnPtr) { return JNI_FALSE;
		}
		jclass clazz = env->FindClass("java/lang/reflect/Method");
		jClassMethod = env->GetMethodID(clazz, "getDeclaringClass", "()Ljava/lang/Class;"); return JNI_TRUE;
	} else { return JNI_FALSE;
	}
}

dalvik_setup还取得了Method类的getDeclaringClass方法ID(jmethodID)。

回到AndFixManager构造函数,它还做了另外一件事情是创建SecurityChecker对象,获取两个信息:安装的app是否是Debug版本,以及app的公钥。

如果需要的话,PatchManager和AndFixManager还创建了自己使用的文件夹,分别是apatch和apatch_opt。

在PatchManager.init()函数中,PatchManager会比较版本号,如果版本不相同说明,用户升级了App以前的补丁就失效了,于是清空以前的补丁。如果版本匹配,PatchManager.init()会调用PatchManager.addPatch(path)来加载补丁,加载的规则就是检查文件夹中是否存在后缀为.apatch的文件。

使用AndFix第二步是,打补丁。打补丁的时机肯定需要越早越好,所以一般放在Application.onCreate()中。

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

loadPatch()第一步先给mLoaders(类型Map<String, ClassLoader>)加入一个ClassLoader。由于是从Context上获取的ClassLoader,这个ClassLoader应该是PathClassLoader。随后,loadPatch()获取需要打补丁的Class列表。然后调用AndFixManager的fix方法。

 public synchronized void fix(File file, ClassLoader classLoader,
			List<String> classes) { if (!mSecurityChecker.verifyApk(file)) {// security check fail return;
		} try {
			File optfile = new File(mOptDir, file.getName()); boolean saveFingerprint = true; if (optfile.exists()) { 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);
					} 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);
		}
	}

AndFixManager的fix方法首先做了安全检查。然后读入patch文件,它实际上就是一个DEX文件,存放在dexFile中。定义了一个patchClassLoader用来加载带有“com.alipay.euler.andfix”前缀的类。这个新的ClassLoader的的Parent ClassLoader应该是前面传入的PathClassLoader。entrys里面存放了补丁DEX文件里面所有的类名。最后遍历entrys,如果该类需要打补丁,就是用刚刚定义的patchClassLoader来装载补丁类。装载成功后,调用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);
			}
		}
	}

fixClass先把类的所有方法找出来。然后检查方法上是否有MethodReplace注解。如果有,说明这是一个需要进行替换处理的方法。注解的定义如下:

@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface MethodReplace { String clazz(); String method();
} 

MethodReplace注解实际是打包工具apkpatch加上去的,对用户透明。clazz表明等待替换方法所在的类名,method表明等待替换的方法名字。fixClass调用replaceMethod。meth是被替换方法,method是替换的方法。replaceMethod的主要任务是获取被替换方法的Method对象。这样将被替换方法和替换方法的Method对象一起交给AndFix.addReplaceMethod处理。

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.addReplaceMethod随后就转入了Native的世界,修改方法指向新替换上来的方法。

extern void __attribute__ ((visibility ("hidden"))) dalvik_replaceMethod(
		JNIEnv* env, jobject src, jobject dest) {
	jobject clazz = env->CallObjectMethod(dest, jClassMethod);
	ClassObject* clz = (ClassObject*) dvmDecodeIndirectRef_fnPtr(
			dvmThreadSelf_fnPtr(), clazz);
	clz->status = CLASS_INITIALIZED;

	Method* meth = (Method*) env->FromReflectedMethod(src);
	Method* target = (Method*) env->FromReflectedMethod(dest);
	LOGD("dalvikMethod: %s", meth->name);

	meth->clazz = target->clazz;
	meth->accessFlags |= ACC_PUBLIC;
	meth->methodIndex = target->methodIndex;
	meth->jniArgInfo = target->jniArgInfo;
	meth->registersSize = target->registersSize;
	meth->outsSize = target->outsSize;
	meth->insSize = target->insSize;

	meth->prototype = target->prototype;
	meth->insns = target->insns;
	meth->nativeFunc = target->nativeFunc;
}

ART的实现似乎更加简单一些。只是版本之间API不咋一致。

回头看Native代码是在哪注册的呢?AndFix有一段静态代码,它会加载andfix。动态链接库加载的时候会调用JNI_OnLoad。

static JNINativeMethod gMethods[] = { /* name, signature, funcPtr */ { "setup", "(ZI)Z", (void*) setup }, { "replaceMethod", "(Ljava/lang/reflect/Method;Ljava/lang/reflect/Method;)V",
		(void*) replaceMethod }, { "setFieldFlag", "(Ljava/lang/reflect/Field;)V", (void*) setFieldFlag }, }; static int registerNatives(JNIEnv* env) { if (!registerNativeMethods(env, JNIREG_CLASS, gMethods,
			sizeof(gMethods) / sizeof(gMethods[0]))) return JNI_FALSE; return JNI_TRUE;
}

前面的分析基于已经存在patch文件进行的。patch文件可能是adb push上去的,也可以是app自己从服务器取的。AndFix提供了patchManager.addPatch(path)来加载它。addPatch的核心是创建Patch对象并调用Patch对象的init方法。

随着DEX文件打包还有Manifest。Manifest文件主要记录了哪些class是需要打补丁的。

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

	}

后面的流程就转入前面提到的loadPatch。

X
赞助一下:
    支付宝    微信    QQ红包

打开支付宝扫一扫
AndFix工作原理
版权声明:若无特殊注明,本文皆为“懒人的小窝”原创,转载请保留文章出处。
本文链接:http://suppore.cn/94.html    百度已收录
正文到此结束

点击下方支持本站

点击支持下贵站吧
点击支持下贵站吧

热门推荐

发表吐槽

你肿么看?

你还可以输入 250 / 250 个字

微笑 可爱 憨笑 鼓掌 白眼 发呆 撇嘴 色 得意 吐 抠鼻 可怜 呲牙 惊讶 冷汗 流泪 大哭 发怒 抚摸 傲慢 惊恐 鄙视 疑问 奸笑 抓狂 偷笑 流汗 擦汗 晕 委屈 吓 衰 糗大了 威武 给力 牛逼

评论信息框

火箭正在发射中...


既然没有吐槽,那就赶紧抢沙发吧!