網(wǎng)站首頁 編程語言 正文
前言
當(dāng)我們寫了一個方法,那么這個方法是如何被執(zhí)行的呢?
public int add(){
int a = 10;
int b = 20;
return a + b;
}
其實方法的本質(zhì)就是arm指令,在Android當(dāng)中,dalvik或者art虛擬機(jī)的執(zhí)行引擎會執(zhí)行arm指令?
?add方法是java代碼,java代碼編譯成class文件,還需要一步轉(zhuǎn)換為dex文件,才能被Android虛擬機(jī)執(zhí)行,dex文件包含了app的所有代碼,因此方法也是存在dex文件中,那么通過dx命令,可以查看方法被編譯成的字節(jié)碼指令
1 arm指令集
dx --dex --verbose --dump-to=dex_method.txt --dump-method=Method.add --verbose-dump Method.class
Android中可以通過dx命令將class文件轉(zhuǎn)換為dex文件,dx.bat位于Android SDK中的build-tools文件夾下,那么可以通過dx命令將class文件翻譯成arm指令集?
可以看一下,打印輸出的arm指令集,ART執(zhí)行某個方法的時候,執(zhí)行的就是這個指令集,當(dāng)apk安裝的時候,dex文件會被dex2oat工具翻譯成本地機(jī)器碼(arm指令集)保存在oat文件中,當(dāng)apk運行的時候oat會被加載到內(nèi)存中,存在虛擬機(jī)的方法區(qū)中?
?執(zhí)行的時候,會構(gòu)建一個棧幀壓入虛擬機(jī)棧中,然后每一個方法在ART中都對應(yīng)一個ArtMethod(這個后邊會說),ArtMethod中的invoke函數(shù)會找到當(dāng)前方法對應(yīng)的本地機(jī)器碼執(zhí)行,執(zhí)行完成之后,棧幀出棧
關(guān)注點回到指令集上,在每一行指令前有一個數(shù)字,代表程序計數(shù)器記錄的行號,精簡之后的指令集(只保留每個行號的最后一個)
Method.add:()I:
regs: 0002; ins: 0001; outs: 0000
0000: const/16 v0, #int 30 // #001e
0002: return v0
0003: code-address
debug info
line_start: 4
parameters_size: 0000
0000: prologue end
0000: line 4
0000: line 6
end sequence
source file: "Method.java"
另外還有一種方式獲取字節(jié)碼,是通過javap獲取,這種跟arm指令有啥區(qū)別呢?其實都是字節(jié)碼,但是javap獲取的字節(jié)碼是JVM執(zhí)行的字節(jié)碼,Android虛擬機(jī)是Dalvik或者Art虛擬機(jī),執(zhí)行的是arm指令集
public int add();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: bipush 10
2: istore_1
3: bipush 20
5: istore_2
6: iload_2
7: iload_1
8: iadd
9: ireturn
LineNumberTable:
line 4: 0
line 5: 3
line 6: 6
這兩者有什么區(qū)別呢?我們看同是執(zhí)行 10 + 20 ,JVM是先創(chuàng)建一個10變量,然后再創(chuàng)建20 ,最后將兩個相加然后返回;但是從ART機(jī)器指令中可以看到是直接計算好了,然后創(chuàng)建v0 = 30,直接返回,所以:Android編譯器在編譯的過程中會做優(yōu)化,提高執(zhí)行的效率(這個可以自己去試一下,javac并沒有做優(yōu)化處理)
當(dāng)一個class類加載進(jìn)來之后,class類中有方法、成員變量等,這些類的信息加載的時候是放在方法區(qū),當(dāng)Java層調(diào)用某個方法時,ART虛擬機(jī)找到該方法對應(yīng)的本地機(jī)器碼指令,在虛擬機(jī)棧中,該方法棧幀入棧,CPU去讀取每行指令,程序計數(shù)器+1,等到方法執(zhí)行完畢,棧幀出棧。
2 AndFix熱修復(fù)原理
之前我們介紹過阿里的AndFix或者Sophix是通過hook native層替換已經(jīng)加載的類的方法,接下來我們著重看一下,AndFix熱修復(fù)是怎么實現(xiàn)的
Method.add:()I:
regs: 0002; ins: 0001; outs: 0000
0000: const/16 v0, #int 30 // #001e
0002: return v0
0003: code-address
debug info
line_start: 4
parameters_size: 0000
0000: prologue end
0000: line 4
0000: line 6
end sequence
source file: "Method.java"
public class Method {
public int add(){
int a = 10;
int b = 20;
return a + b;
}
}
//調(diào)用
Method method = new Method();
method.add();
我們看下這個方法,通過Method對象去調(diào)用,method是在堆內(nèi)存中,通過對象可以拿到類信息在方法區(qū)中。?
當(dāng)執(zhí)行這個方法時,ART執(zhí)行引擎從方法區(qū)中找到方法的本地機(jī)器指令,通過CPU執(zhí)行得到結(jié)果,如果add方法中拋出異常導(dǎo)致app崩潰,那么如何修復(fù)?
2.1 ArtMethod
既然要做到方法替換,首先必須要了解方法在虛擬機(jī)中的形態(tài);其實前面有提到,方法在虛擬機(jī)中對應(yīng)的結(jié)構(gòu)體就是ArtMethod,每個方法在ART中對應(yīng)一個ArtMethod。
# Android 10.0/art/runtime/art_method.h
protected:
GcRoot<mirror::Class> declaring_class_;
std::atomic<std::uint32_t> access_flags_;
uint32_t dex_code_item_offset_;
uint32_t dex_method_index_;
uint16_t method_index_;
union {
uint16_t hotness_count_;
uint16_t imt_index_;
};
// Fake padding field gets inserted here.
// Must be the last fields in the method.
struct PtrSizedFields {
// Depending on the method type, the data is
// - native method: pointer to the JNI function registered to this method
// or a function to resolve the JNI function,
// - conflict method: ImtConflictTable,
// - abstract/interface method: the single-implementation if any,
// - proxy method: the original interface method or constructor,
// - other methods: the profiling data.
void* data_;
// Method dispatch from quick compiled code invokes this pointer which may cause bridging into
// the interpreter.
void* entry_point_from_quick_compiled_code_;
} ptr_sized_fields_;
在ArtMethod中,有一個結(jié)構(gòu)體PtrSizedFields,其中一個成員變量為entry_point_from_quick_compiled_code_,這個指針指向的就是在方法區(qū)中該方法本地機(jī)器碼的內(nèi)存地址,也就是說,如果想要實現(xiàn)熱修復(fù),那么就將entry_point_from_quick_compiled_code_指向正確的方法機(jī)器碼指令地址即可。
除此之外,看下其他成員變量的含義:
- declaring_class_:用來標(biāo)記當(dāng)前方法屬于哪個類
- access_flags_:當(dāng)前方法的訪問修飾符
- hotness_count_:記錄當(dāng)前方法被調(diào)用的次數(shù),如果超過某個限制,那么該方法就被標(biāo)記為是熱方法,這個與ART的編譯模式相關(guān)
對于hotness_count_,這里需要說一下ART的編譯模式,Dalvik的就先不介紹了
2.2 ART編譯模式
在Android 5.0之后,Android編譯器由ART代替了Dalvik,采用了全新的編譯模式AOT,代替JIT;
什么是AOT?就是全量編譯,在APK安裝的時候,會將所有的dex文件編譯成本地機(jī)器碼,然后在執(zhí)行方法時會直接拿到相應(yīng)的機(jī)器碼執(zhí)行,速度非常快,但是這也帶來一些問題:
(1)安裝時間長
因為在安裝的過程中做全量的編譯,耗時非常嚴(yán)重;早先的Android手機(jī)我們在安裝的時候,進(jìn)度條一直在轉(zhuǎn)但就是裝不上,這種是非常差的用戶體驗
(2)存儲空間
因為全量編譯的時候,dex被編譯成機(jī)器碼之后,保存在.oat文件中,10M的dex翻譯成的機(jī)器碼內(nèi)存激增4-5倍,大量的文件保存在手機(jī)中會占據(jù)內(nèi)存空間
所以在Android N之后,采用了混合編譯模式,AOP + 解釋 + JIT
全新的混編模式不再在APK安裝的時候進(jìn)行全量編譯,而是會解釋字節(jié)碼,因此安裝的速度很快;此外新增了一個JIT編譯器,會在App運行的時候分析代碼,把結(jié)果保存在Profile中,并且在空閑時間分析并編譯這些代碼;
接著上面的hotness_count_,其實用來記錄這個方法被調(diào)用的次數(shù),當(dāng)超過某個閾值之后,這個方法會被標(biāo)記為熱代碼,這些熱方法在設(shè)備空閑的時候做編譯,并保存在名為app_image的base.art文件中,這個art文件會在類加載之前加載到內(nèi)存中,意味著當(dāng)調(diào)用這個方法的時候,不再需要編譯為機(jī)器碼,而是直接執(zhí)行拿到結(jié)果。
2.3 AndFix框架實現(xiàn)
首先創(chuàng)建一個C++的模塊,然后C++版本可選擇個人熟悉的,我對C++ 11的一些特性比較熟悉?
?其實AndFix實現(xiàn)的關(guān)鍵,就是找到ArtMethod,在JNI層是能夠?qū)崿F(xiàn)的,通過JNIEnv的FromReflectedMethod函數(shù)
public class AndFixManager {
//native熱修復(fù)方法
public static native void fix(Method wrong, Method right);
}
//fix對應(yīng)的JNI接口
extern "C"
JNIEXPORT void JNICALL
Java_com_tal_andfix_AndFixManager_fix(
JNIEnv *env,
jclass clazz,
jobject wrong,
jobject right) {
//獲取ArtMethod
env->FromReflectedMethod(wrong);
}
其實在Java層調(diào)用的時候,是需要反射獲取某個方法,也就是說,在Java層反射拿到的方法其實就是ArtMethod,只不過再底層的我們看不到,那現(xiàn)在就能看到了!
try {
Class<?> clazz = Class.forName("com.tal.demo02.FixDemo");
Method run = clazz.getDeclaredMethod("run");
AndFixManager.fix(run,run);
} catch (Exception e) {
e.printStackTrace();
}
2.3.1 獲取ArtMethod
之前我們看源碼的時候,可以看到ArtMethod.h中存在很多系統(tǒng)的頭文件,全部導(dǎo)入工程中不現(xiàn)實?
?因為我們需要的是ArtMethod的一個結(jié)構(gòu)體的成員變量,所以我們只需要針對性地導(dǎo)入即可,art_method.h如下;
#ifndef DEMO02_ART_METHOD_H
#define DEMO02_ART_METHOD_H
#endif //DEMO02_ART_METHOD_H
#include "stdint.h"
namespace art{
namespace mirror{
class ArtMethod final {
public:
uint32_t declaring_class_;
std::atomic<std::uint32_t> access_flags_;
uint32_t dex_code_item_offset_;
uint32_t dex_method_index_;
uint16_t method_index_;
union {
uint16_t hotness_count_;
uint16_t imt_index_;
};
struct PtrSizedFields {
void* data_;
void* entry_point_from_quick_compiled_code_;
} ptr_sized_fields_;
};
}
}
最終在Java層調(diào)用JNI方法,執(zhí)行到JNI層,獲取到ArtMethod
extern "C"
JNIEXPORT void JNICALL
Java_com_tal_andfix_AndFixManager_fix(
JNIEnv *env,
jclass clazz,
jobject wrong,
jobject right) {
//獲取ArtMethod
ArtMethod *artMethod = reinterpret_cast<ArtMethod *>(env->FromReflectedMethod(wrong));
}
這里通過斷點可以看到,ArtMethod已經(jīng)拿到了,而且關(guān)鍵信息entry_point_from_quick_compiled_code_,也就是arm指令集的內(nèi)存地址拿到了!?
2.3.2 方法替換
public class FixDemo {
public void run(){
throw new IllegalArgumentException();
}
}
public class FixDemo {
public void run(){
Log.e("TAG","已經(jīng)被修復(fù)了");
}
}
現(xiàn)在有一個場景就是,當(dāng)執(zhí)行FixDemo的run方法時拋出異常導(dǎo)致崩潰,這種場景下,使用熱修復(fù)技術(shù)怎么修復(fù)呢,就是方法替換,arm指令集替換
public class AndFixManager {
public static void bugFix(){
try {
Class clazz = Class.forName("com.take.andfix.FixDemo");
Method wrong = clazz.getDeclaredMethod("run");
//正確的方法
Class clazz1 = Class.forName("com.take.andfix.fox.FixDemo");
Method right = clazz1.getDeclaredMethod("run");
AndFixManager.fix(wrong, right);
}catch (Exception e){
}
}
public static native void fix(Method wrong, Method right);
}
拋出異常的類是andfix包下的,當(dāng)線上需要修復(fù)時,下發(fā)patch包,然后加載fox包下的方法,調(diào)用native fix方法
extern "C"
JNIEXPORT void JNICALL
Java_com_tal_andfix_AndFixManager_fix(JNIEnv *env, jclass clazz, jobject wrong, jobject right) {
//獲取ArtMethod
ArtMethod *wrongMethod = reinterpret_cast<ArtMethod *>(env->FromReflectedMethod(wrong));
ArtMethod *rightMethod = reinterpret_cast<ArtMethod *>(env->FromReflectedMethod(right));
//方法替換
wrongMethod->declaring_class_ = rightMethod->declaring_class_;
wrongMethod->access_flags_ = rightMethod->access_flags_;
wrongMethod->dex_code_item_offset_ = rightMethod->dex_code_item_offset_;
wrongMethod->dex_method_index_ = rightMethod->dex_method_index_;
wrongMethod->method_index_ = rightMethod->method_index_;
wrongMethod->ptr_sized_fields_.data_ = rightMethod->ptr_sized_fields_.data_;
wrongMethod->ptr_sized_fields_.entry_point_from_quick_compiled_code_ = rightMethod->ptr_sized_fields_.entry_point_from_quick_compiled_code_;
}
然后再次執(zhí)行run方法
binding.sampleText.setOnClickListener {
AndFixManager.bugFix()
val fixDemo = FixDemo()
fixDemo.run()
}
打印出的結(jié)果:E/TAG: 已經(jīng)被修復(fù)了
其實現(xiàn)在阿里的AndFix和Sophix已經(jīng)不維護(hù)了,但是這種熱修復(fù)的思想我們是需要了解的,尤其是通過hook native底層替換方法,能夠幫助我們更好地了解JVM虛擬機(jī)和Android虛擬機(jī)。
2.4 AndFix動態(tài)化配置
在上面簡單的demo中,我們是知道那個類的哪個方法發(fā)生異常,在代碼中寫死的,但真正的線上環(huán)境中,其實是不知道哪個類會報錯,一般我們都會使用bugly,像crash跟anr都能夠?qū)崟r監(jiān)控到?
?當(dāng)app某個方法拋異常之后,通過bugly上報到后臺,比如com.take.andfix.FixDemo這個類中的run方法拋出了異常,那么我們需要針對這個類的方法做修復(fù),如果做到動態(tài)化,需要使用注解修飾這個修復(fù)類
/**
* 修復(fù)類需要使用這個注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface andfix {
String clazz();
String method();
}
public class FixDemo {
@andfix(clazz = "com.tal.andfix.FixDemo",method = "run")
public void run(){
Log.e("TAG","已經(jīng)被修復(fù)了");
}
}
這樣在熱修復(fù)時,能夠知道這個修復(fù)類要修復(fù)線上環(huán)境中那個類的哪個方法
2.4.1 dex打包
在打包dex的時候,需要把整個包名路徑下的class文件一起打包,通過命令行完成dex打包
dx --dex --output fix.dex /xxxx/Desktop/dx
?將打包成功的dex修復(fù)包,放到sd卡中 :
2.4.2 dex文件加載
dex文件的加載,通過DexFile實現(xiàn),如果不熟悉可以看下源碼,art虛擬機(jī)會將dex轉(zhuǎn)換為odex,因此加載dex文件的時候,需要傳入一個odex文件的緩存路徑。
將dex文件加載到內(nèi)存之后,可以獲取到dex文件中全部的類,通過DexFile.loadClass就可以將這個類通過類加載器加載。
/**
* dex文件加載,將dex文件加載到內(nèi)存
* @param context
* @param dexFile
*/
private static void loadFixDex(Context context, File dexFile) {
try {
DexFile odex = DexFile.loadDex(
dexFile.getAbsolutePath(),
new File(context.getCacheDir(), "odex").getAbsolutePath(),
Context.MODE_PRIVATE
);
Enumeration<String> entries = odex.entries();
while (entries.hasMoreElements()){
//全類名
String clazzName = entries.nextElement();
//加載類
Class aClass = odex.loadClass(clazzName, context.getClassLoader());
//處理類
if(aClass != null){
processClass(aClass);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
這里會有一個問題,就是既然拿到了全類名,為什么不能通過方式1獲取,而是需要通過方式2獲取?原因就是,Class.forName是從當(dāng)前apk中查找這個類,但是這個類是在dex文件中,是從服務(wù)端下發(fā)的,并沒有放在apk中,因此通過Class.forName是找不到的,通過DexFile.loadClass才是真正加載類到了內(nèi)存中
//方式1
Class.forName("xxxxxxxxxx")
//方式2
odex.loadClass(clazzName, context.getClassLoader())
2.4.3 動態(tài)替換方法
拿類之后,通過反射能夠拿到修復(fù)類中的方法,當(dāng)然不是每個方法都是需要被修復(fù)的,我們需要判斷的是,上面是否有我們自定義的注解,如果有,那么就能夠通過反射,拿到拋出異常的這個方法,因為注解上有我們傳入的類名和方法名,最終調(diào)用JNI的接口實現(xiàn)動態(tài)替換方法
private static void processClass(Class aClass) {
//獲取方法上的注解
Method[] methods = aClass.getMethods();
for (Method method:methods){
andfix annotation = method.getAnnotation(andfix.class);
if(annotation != null){
//如果存在這個注解,那么就執(zhí)行方法替換
String clazz = annotation.clazz();
String method1 = annotation.method();
//獲取wrong方法
try {
Class<?> wrongMethodClass = Class.forName(clazz);
//這里注意,修復(fù)類的方法,要和被修復(fù)的方法,參數(shù)一致!!!!!
Method wrongMethod = wrongMethodClass.getDeclaredMethod(method1,method.getParameterTypes());
//動態(tài)方法替換
fix(wrongMethod,method);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
2.4.4 文件訪問問題
一切準(zhǔn)備就緒之后,可以通過加載dex補丁包來修復(fù)
binding.sampleText.setOnClickListener {
// AndFixManager.bugFix()
AndFixManager.loadFixDex(
this,
File(System.getenv("EXTERNAL_STORAGE"), "fix.dex")
)
val fixDemo = FixDemo()
fixDemo.run()
}
這里可能會碰到一些加載SD卡中文件報錯的問題,比如:
No original dex files found for dex location /sdcard/fix.dex
這里需要添加文件的讀寫權(quán)限,才能夠保證有效的熱修復(fù),除此之外,在Android 10以上的版本,需要在清單文件中添加android:requestLegacyExternalStorage屬性
android:requestLegacyExternalStorage="true"
通過這種hook native底層的方式,最大的優(yōu)勢在于能夠真正實現(xiàn)熱修復(fù),不需要重新啟動app就能夠修復(fù),但是存在的弊端也是比較明顯的,就是兼容性問題,每個Android的版本,native層都會有變化,比如art_method.h,其實每個版本都是不一樣的,我這次使用的就是Android 10中的art_method頭文件,有興趣的可以看看之前Android版本的頭文件,其實還是有差別的,所以在做兼容性問題的時候,需要根據(jù)版本來適配不同的頭文件
原文鏈接:https://juejin.cn/post/7100079518756552734
相關(guān)推薦
- 2022-11-13 Error: EACCES: permission denied, access '/usr/loc
- 2023-07-05 【Redis】數(shù)據(jù)被刪除,內(nèi)存占用還這么大?
- 2022-08-13 404究竟是什么意思呢?像404,200,503等數(shù)字究竟是什么東西
- 2022-12-05 Golang中的錯誤處理的示例詳解_Golang
- 2022-10-03 react實現(xiàn)數(shù)據(jù)監(jiān)聽方式_React
- 2022-04-25 Pycharm下載pyinstaller報錯:You?should?consider?upgradi
- 2022-09-10 nginx?Rewrite重寫地址的實現(xiàn)_nginx
- 2022-08-21 .Net彈性和瞬態(tài)故障處理庫Polly實現(xiàn)彈性策略_實用技巧
- 最近更新
-
- window11 系統(tǒng)安裝 yarn
- 超詳細(xì)win安裝深度學(xué)習(xí)環(huán)境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎(chǔ)操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區(qū)別,Jav
- spring @retryable不生效的一種
- Spring Security之認(rèn)證信息的處理
- Spring Security之認(rèn)證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權(quán)
- redisson分布式鎖中waittime的設(shè)
- maven:解決release錯誤:Artif
- restTemplate使用總結(jié)
- Spring Security之安全異常處理
- MybatisPlus優(yōu)雅實現(xiàn)加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務(wù)發(fā)現(xiàn)-Nac
- Spring Security之基于HttpR
- Redis 底層數(shù)據(jù)結(jié)構(gòu)-簡單動態(tài)字符串(SD
- arthas操作spring被代理目標(biāo)對象命令
- Spring中的單例模式應(yīng)用詳解
- 聊聊消息隊列,發(fā)送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠(yuǎn)程分支