熱修復實現原理——native hook
一、native hook簡介
直接在native層進行方法的結構體信息對換,從而實現完美的方法新舊替換,從而實現熱修復功能。例如AndFix采用native hook的方式,以Field為切入點,直接使用dalvik_replaceMethod替換class中方法的實現。由于它并沒有整體替換class, 而field在class中的相對地址在class加載時已確定,所以AndFix無法支持新增或者刪除filed的情況(通過替換init與clinit只可以修改field的數值)。
在dalvik上的實現略有不同,是通過jni bridge來指向補丁的方法。
二、修復過程(以AndFix為例)
I、Java層
java 層的功能就是找到補丁文件,根據補丁中的注解找到將要替換的方法然后交給jni層去處理替換方法的操作
1、application 初始化
public class MainApplication extends Application {
private static final String TAG = " andrew";
private static final String APATCH_PATH = "/out.apatch";
private static final String DIR = "apatch";//補丁文件夾
/**
* patch manager
*/
private PatchManager mPatchManager;
@Override
public void onCreate() {
super.onCreate();
// initialize
mPatchManager = new PatchManager(this);
mPatchManager.init("1.0");
Log.d(TAG, "inited.");
// load patch
mPatchManager.loadPatch();
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);
}
}
}
2、實例化PatchManager
//SP_VERSION 更多象征app的版本,該值不變時,打補丁;改變時,清空補丁
// patch extension
private static final String SUFFIX = ".apatch";//后綴名
private static final String DIR = "apatch";//補丁文件夾
private static final String SP_NAME = "_andfix_";
private static final String SP_VERSION = "version";//熱更新補丁時,版本不變,自動加載補丁;apk完整更新發布時,版本提升,本地會自動刪除以前加載在apatch文件夾里的補丁,防止二次載入過時補丁
/**
* context
*/
private final Context mContext;
/**
* AndFix manager
*/
private final AndFixManager mAndFixManager;
/**
* patch directory
*/
private final File mPatchDir;
/**
* patchs
*/
private final SortedSet mPatchs;
/**
* classloaders
*/
private final Map mLoaders;
/**
* @param context context
*/
public PatchManager(Context context) {
mContext = context;
mAndFixManager = new AndFixManager(mContext);//初始化AndFixManager
mPatchDir = new File(mContext.getFilesDir(), DIR);//初始化存放patch補丁文件的文件夾, data/data/包名/files/patch
mPatchs = new ConcurrentSkipListSet();//初始化存在Patch類的集合,此類適合大并發
mLoaders = new ConcurrentHashMap();//初始化存放類對應的類加載器集合
}
3、初始化AndFixManager
此處 主要在native層進行 ;一件事是判斷當前環境是否支持熱修復,一件事是初始化修復包安全校驗的工作
4、初始化PatchManager
就是從SharedPreferences讀取以前存的版本和你傳過來的版本進行比對,如果兩者版本不一致就刪除本地patch,否則調用initPatchs()這個方法
/**
* initialize
*
* @param appVersion App version
*/
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()) {//如果遇到同名的文件,則將該同名文件刪除
mPatchDir.delete();
return;
}
//在該文件下放入一個名為_andfix_的SharedPreferences文件
SharedPreferences sp = mContext.getSharedPreferences(SP_NAME,
Context.MODE_PRIVATE);//存儲關于patch文件的信息
//根據你傳入的版本號和之前的對比,做不同的處理
String ver = sp.getString(SP_VERSION, null);
//根據版本號加載補丁文件,版本號不同清空緩存目錄
if (ver == null || !ver.equalsIgnoreCase(appVersion)) {
cleanPatch();//刪除本地patch文件
sp.edit().putString(SP_VERSION, appVersion).commit();//并把傳入的版本號保存
} else {
initPatchs();//初始化patch列表,把本地的patch文件加載到內存
}
}
5、加載patch文件到內存
把擴展名為.apatch的文件加載到內存,初始化對應的Patch,并把剛初始化的Patch加入到我們之前看到的Patch集合mPatchs中
private void initPatchs() {
File[] files = mPatchDir.listFiles();
for (File file : files) {
addPatch(file);
}
}
/**
* add patch file
*
* @param file
* @return patch
*/
private Patch addPatch(File file) {
Patch patch = null;
if (file.getName().endsWith(SUFFIX)) {
try {
patch = new Patch(file);//實例化Patch對象
mPatchs.add(patch);//把patch實例存儲到內存的集合中,在PatchManager實例化集合
} catch (IOException e) {
Log.e(TAG, "addPatch", e);
}
}
return patch;
}
/**
Patch文件的實例化
*/
public class Patch implements Comparable {
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";
/**
* patch file
*/
private final File mFile;
/**
* name
*/
private String mName;
/**
* create time
*/
private Date mTime;
/**
* classes of patch
*/
private Map> mClassesMap;
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);//使用JarFile讀取Patch文件
JarEntry entry = jarFile.getJarEntry(ENTRY_NAME);//獲取META-INF/PATCH.MF文件
inputStream = jarFile.getInputStream(entry);
Manifest manifest = new Manifest(inputStream);
Attributes main = manifest.getMainAttributes();
mName = main.getValue(PATCH_NAME);//獲取PATCH.MF屬性Patch-Name
mTime = new Date(main.getValue(CREATED_TIME));//獲取PATCH.MF屬性Created-Time
mClassesMap = new HashMap>();
Attributes.Name attrName;
String name;
List strings;
for (Iterator it = main.keySet().iterator(); it.hasNext();) {
attrName = (Attributes.Name) it.next();
name = attrName.toString();
//判斷name的后綴是否是-Classes,并把name對應的值加入到集合中,對應的值就是class類名的列表
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();
}
}
}
public String getName() {
return mName;
}
public File getFile() {
return mFile;
}
public Set getPatchNames() {
return mClassesMap.keySet();
}
public List getClasses(String patchName) {
return mClassesMap.get(patchName);
}
public Date getTime() {
return mTime;
}
@Override
public int compareTo(Patch another) {
return mTime.compareTo(another.getTime());
}
}
6、對比查找不同
調用PatchManager. loadPatch;遍歷mPatchs中每個patch的每個類,mPatchs就是上文介紹的存儲patch的一個集合。根據補丁名找到對應的類,做為參數傳給fix()
/**
* load patch,call when application start
*/
public void loadPatch() {
mLoaders.put("*", mContext.getClassLoader());// wildcard
Set patchNames;
List classes;
for (Patch patch : mPatchs) {
patchNames = patch.getPatchNames();
for (String patchName : patchNames) {
classes = patch.getClasses(patchName);
mAndFixManager.fix(patch.getFile(), mContext.getClassLoader(),
classes);
}
}
}
7、跟修改情況調用Native
/**
* fix
*
* @param file
* patch file
* @param classLoader
* classloader of class that will be fixed
* @param classes
* classes will be fixed
*/
public synchronized void fix(File file, ClassLoader classLoader,
List classes) {
if (!mSupport) {
return;
}
//判斷patch文件的簽名
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;
}
}
//加載patch文件中的dex
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 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);//獲取有bug的類文件
if (clazz != null) {
fixClass(clazz, classLoader);
}
}
} catch (IOException e) {
Log.e(TAG, "pacth", e);
}
}
/**
* fix class
*
* @param clazz
* class
*/
private void fixClass(Class clazz, ClassLoader classLoader) {
//使用反射獲取這個類中所有的方法
Method[] methods = clazz.getDeclaredMethods();
//MethodReplace是這個庫自定義的Annotation,標記哪個方法需要被替換
MethodReplace methodReplace;
String clz;
String meth;
for (Method method : methods) {
//獲取此方法的注解,因為有bug的方法在生成的patch的類中的方法都是有注解的
//還記得對比過程中生成的Annotation注解嗎
//這里通過注解找到需要替換掉的方法
methodReplace = method.getAnnotation(MethodReplace.class);
if (methodReplace == null)
continue;
clz = methodReplace.clazz();//獲取注解中clazz的值,標記的類
meth = methodReplace.method();//獲取注解中method的值,需要替換的方法
if (!isEmpty(clz) && !isEmpty(meth)) {
//所有找到的方法,循環替換
replaceMethod(classLoader, clz, meth, method);
}
}
}
/**
* replace method
*
* @param classLoader classloader
* @param clz class
* @param meth name of target method
* @param method source method
*/
private void replaceMethod(ClassLoader classLoader, String clz,
String meth, Method method) {
try {
String key = clz + "@" + classLoader.toString();
Class clazz = mFixedClass.get(key);//判斷此類是否被fix
if (clazz == null) {// class not load
Class clzz = classLoader.loadClass(clz);
// initialize target class
clazz = AndFix.initTargetClass(clzz);//初始化class
}
if (clazz != null) {// initialize class OK
mFixedClass.put(key, clazz);
Method src = clazz.getDeclaredMethod(meth,
method.getParameterTypes());//根據反射獲取到有bug的類的方法(有bug的apk)
AndFix.addReplaceMethod(src, method);//src是有bug的方法,method是補丁方法
}
} catch (Exception e) {
Log.e(TAG, "replaceMethod", e);
}
}
//調用jni替換,src是有bug的方法,method是補丁方法
private static native boolean setup(boolean isArt, int apilevel);
private static native void replaceMethod(Method dest, Method src);
private static native void setFieldFlag(Field field);
public static void addReplaceMethod(Method src, Method dest) {
try {
replaceMethod(src, dest);//調用了native方法,next code
initFields(dest.getDeclaringClass());
} catch (Throwable e) {
Log.e(TAG, "addReplaceMethod", e);
}
}
II、Native層
由于android 4.4 之后新增應用運行模式 此處分兩種
在native層中會做art和dalvik虛擬機的區分處理工作,Java hook大致的邏輯都是一致的:
1、dalvik 模式
- 在libdvm.so動態獲取dvmDecodeIndirectRef函數指針和獲取dvmThreadSelf函數指針。
- 調用dest的 Method.getDeclaringClass方法獲取method的類對象clazz。
- 調用dvmDecodeIndirectRef方法,獲取clazz的ClassObject*
- 通關 env->FromReflectedMethod方法獲取dest的Method結構體函數的指針
- 替換method結構體的成員數據
2、art 模式
- art模式中,我們直接通過 env->FromReflectedMethod獲取到ArtMethod函數指針。
- 然后直接替換ArtMethod結構體的成員數據指針
三、native hook 優劣
因為是動態的,所以不需要重啟應用就可以生效
支持ART與Dalvik
與multidex方案相比,性能會有所提升(Multi Dex需要修改所有class的class_ispreverified標志位,導致運行時性能有所損失)
支持新增加方法
-
支持在新增方法中新增局部變量
?
支持的補丁場景相對有限,僅僅可以使用它來修復特定問題(兼容性較差)一般只用來修復方法。類的成員字段不能修改