背景
目前大多數(shù)hotfix框架都是通過runtime+其他語言引擎來實(shí)現(xiàn)的论熙,當(dāng)然也有像手Q這樣自己編譯福青、解析字節(jié)碼的,目前我知道的一些公司在用的有ruby脓诡、lua无午、javascript,基本原理就是利用這些語言引擎與OC通信祝谚,再通過runtime完成方法調(diào)用宪迟,這樣就能用其他語言來寫OC的代碼了。仔細(xì)想想這里的引擎起到了什么作用交惯?無非是一個(gè)代碼運(yùn)行的環(huán)境次泽,簡(jiǎn)單來說就是棧+基本語法支持穿仪,這里的棧是用來記錄方法運(yùn)行產(chǎn)生的變量的,另外在OC中意荤,大部分實(shí)現(xiàn)都是通過調(diào)用OC方法以及配合if-else啊片、循環(huán)來實(shí)現(xiàn)的,所以下面要介紹的熱修復(fù)框架的基本思想就是當(dāng)修復(fù)一個(gè)方法時(shí)玖像,為這個(gè)方法生產(chǎn)一個(gè)環(huán)境池紫谷,用來存放方法內(nèi)部產(chǎn)生的變量;方法的實(shí)現(xiàn)是N條消息組成捐寥,相當(dāng)于N個(gè)方法調(diào)用笤昨,這些調(diào)用產(chǎn)生的變量和參數(shù)通過環(huán)境池存取握恳;再加上if-else瞒窒、while的支持,就實(shí)現(xiàn)了一個(gè)簡(jiǎn)易版的熱修復(fù)框架睡互。
InstructionPatch
InstructionPatch是一個(gè)不依賴其他語言引擎的熱修復(fù)框架根竿,通過下發(fā)json文件,再利用runtime來完成熱修復(fù)就珠。它的基本原理是修改forwardInvocation:
寇壳,使其指向自己的實(shí)現(xiàn),當(dāng)要修復(fù)某個(gè)方法時(shí)妻怎,讓它轉(zhuǎn)發(fā)到自己實(shí)現(xiàn)的forwardInvocation:
中壳炎。方法的實(shí)現(xiàn)由一系列消息組成,消息之間的參數(shù)逼侦、變量通過一個(gè)環(huán)境池(Map)傳遞匿辩,這個(gè)環(huán)境池會(huì)在方法結(jié)束時(shí)自動(dòng)清空。
不依賴其他語言引擎好處有:
- 不需要引入多余的引擎
- 支持的系統(tǒng)版本更多
- 可控性強(qiáng)榛丢,無論是對(duì)象轉(zhuǎn)換還是引用管理铲球,當(dāng)然做的也多
壞處:
- 基本語法不支持,諸如if-else晰赞、while也需要自己實(shí)現(xiàn)
- 熱修復(fù)代碼可讀性差稼病,當(dāng)然可以通過腳本自動(dòng)生成json來優(yōu)化
- 修復(fù)的方法內(nèi)部實(shí)現(xiàn)只能是OC方法的調(diào)用
示例
@implementation IPViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
+ (NSString *)returnClassMethod {
return NSStringFromSelector(_cmd);
}
- (void)logObject:(NSString *)obj {
NSLog(@"%@", obj);
}
@end
現(xiàn)在需要在viewDidLoad中打印returnClassMethod中返回的字符串只需要以下指令:
{
// 所有修復(fù)指令
"instructions": [
{
// 被修復(fù)的類
"cls": "IPViewController",
// 被修復(fù)方法
"methodList": [
{
// 修復(fù)的方法selector
"method": "viewDidLoad",
"isStatic": false,
// 修復(fù)后的方法實(shí)現(xiàn)
"messages": [
{
// [super viewDidLoad]
"receiver": "super",
"message": "viewDidLoad"
},
{
// NSString *logStr = [IPViewController returnClassMethod];
// logStr將會(huì)被存入環(huán)境池
"returnType": "NSString",
"returnObj": "logStr",
"receiver": "IPViewController",
"isStatic":true,
"message": "returnClassMethod"
},
{
// [self logObject:logStr];
"receiver": "self",
"message": "logObject:",
"args": [
{
// 從環(huán)境池中取logStr這個(gè)對(duì)象
"valueKey": "logStr"
}
]
}
]
}
]
}
]
}
詳情使用方式參見使用文檔。
技術(shù)實(shí)現(xiàn)
EnvironmentPool
環(huán)境池實(shí)際上就是一個(gè)全局的靜態(tài)Map掖鱼,用key-value的形式存取然走,當(dāng)進(jìn)入一個(gè)方法時(shí),自動(dòng)為這個(gè)方法開辟一塊空間來存放產(chǎn)生的變量戏挡,退出方法時(shí)再清空這塊空間芍瑞。但實(shí)際上,方法實(shí)現(xiàn)中經(jīng)常有異步的block褐墅,這時(shí)候方法結(jié)束了并不能立即清空拆檬,否則block真正在執(zhí)行的時(shí)候就沒地方去取相應(yīng)的變量了洪己。為了解決這個(gè)問題,借鑒了一下OC的引用計(jì)數(shù)秩仆,當(dāng)方法開始時(shí)引用+1码泛,發(fā)現(xiàn)有block時(shí)引用也+1猾封,方法執(zhí)行結(jié)束澄耍、block執(zhí)行結(jié)束引用-1,這樣就能避免環(huán)境池過早釋放的問題了晌缘。但是這又引出了另外一個(gè)問題齐莲,像一個(gè)網(wǎng)絡(luò)請(qǐng)求一般有success和failure兩個(gè)block,但是最終卻只有一個(gè)能被執(zhí)行磷箕,這就導(dǎo)致引用計(jì)數(shù)始終大于0选酗,而且在代碼上并不能判斷一個(gè)block會(huì)不會(huì)被執(zhí)行,所以只能是讓用戶手動(dòng)在json的message中手動(dòng)指定引用的次數(shù)environmentPoolRefCount岳枷。
既然environmentPool是個(gè)Map芒填,那么就只能存取id類型的變量,所以對(duì)一些基本類型的變量要做一層包裝空繁,在使用的時(shí)候再解包殿衰。變量類型主要通過NSMethodSignature中的信息根據(jù)Type Encodings來判斷。
Block的實(shí)現(xiàn)
在具體的業(yè)務(wù)中盛泡,block要么作為被修復(fù)方法參數(shù)要被調(diào)用闷祥,要么作為被調(diào)用方法的參數(shù)要被構(gòu)造,這樣問題就變成了:
- 如何調(diào)用參數(shù)傲诵、返回值不確定的block變量
- 如何構(gòu)造參數(shù)類型凯砍、個(gè)數(shù)不確定block變量
要確定一個(gè)方法的參數(shù)、返回值信息拴竹,首先就要知道這個(gè)方法簽名悟衩,block也一樣,所以第一個(gè)問題的核心就是拿到block變量的簽名栓拜。雖然系統(tǒng)沒提供座泳,不過github很多庫和runtime源碼中都有相關(guān)的實(shí)現(xiàn),具體代碼如下:
struct IPBlockLayout {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct IPBlockDescriptor *descriptor;
};
struct IPBlockDescriptor {
unsigned long int reserved;
unsigned long int size;
void (*copy)(void *dst, void *src);
void (*dispose)(void *src);
const char *signature;
};
enum {
IP_BLOCK_HAS_COPY_DISPOSE = (1 << 25),
IP_BLOCK_HAS_SIGNATURE = (1 << 30)
};
static NSMethodSignature * _IPBlockSignature(id block) {
struct IPBlockLayout *bp = (__bridge struct IPBlockLayout *)block;
if (bp && (bp->flags & IP_BLOCK_HAS_SIGNATURE)) {
void *signatureLocation = bp->descriptor;
signatureLocation += sizeof(unsigned long int);
signatureLocation += sizeof(unsigned long int);
if (bp->flags & IP_BLOCK_HAS_COPY_DISPOSE) {
signatureLocation += sizeof(void(*)(void *dst, void *src));
signatureLocation += sizeof(void (*)(void *src));
}
const char *signature = (*(const char **)signatureLocation);
NSMethodSignature *blockSignature = [NSMethodSignature signatureWithObjCTypes:signature];
return blockSignature;
}
return nil;
}
拿到簽名后菱属,一切就變得簡(jiǎn)單了钳榨,直接利用NSInvocation
來調(diào)用就好。不過這里仍要注意一點(diǎn):一般方法的參數(shù)都是都是從index=2開始設(shè)置的纽门,前兩個(gè)分別是self和selector薛耻,但是blcok因?yàn)闆]有selector(這里沒找到什么資料,僅僅是我猜測(cè)的原因)赏陵,所以要從index=1開始設(shè)置饼齿。
第二個(gè)問題就很麻煩了饲漾,沒有簽名,也沒有一個(gè)通用的類型來代表id缕溉、int考传、double、float等等证鸥,只能退而求其次僚楞,使用void *
,且最多支持4個(gè)參數(shù)枉层,這點(diǎn)和JSPatch遇到的問題一樣泉褐。這樣block就有很大的限制了,但是看起來也是夠用了鸟蜡。
數(shù)據(jù)結(jié)構(gòu)
@protocol IPIntructionArgumentModelProtocol<NSObject>
@property (nonatomic, copy) NSString *type;
@property (nonatomic, copy) NSString *valueKey;
@property (nonatomic, copy) NSString *stringValue;
@property (nonatomic, assign) double digital;
@property (nonatomic, copy) NSString *digitalType;
@property (nonatomic, copy) NSArray *blockParameterTypes;
@property (nonatomic, copy) NSString *blockParameterPrefix;
@property (nonatomic, copy) NSArray<id<IPIntructionMessageModelProtocol>> *innerMessage;
@end
@protocol IPIntructionMessageModelProtocol<NSObject>
@property (nonatomic, copy) NSString *returnType;
@property (nonatomic, copy) NSString *returnObj;
@property (nonatomic, copy) NSString *receiver;
@property (nonatomic, copy) NSString *message;
@property (nonatomic, assign) BOOL isStatic;
@property (nonatomic, assign) BOOL isBlock;
@property (nonatomic, assign) BOOL isIfSnippet;
@property (nonatomic, assign) BOOL isWhileSnippet;
@property (nonatomic, assign) BOOL isReturnSnippet;
@property (nonatomic, assign) NSInteger environmentPoolRefCount;
@property (nonatomic, copy) NSString *blockKey;
@property (nonatomic, copy) NSArray<id<IPIntructionArgumentModelProtocol>> *args;
@end
@protocol IPIntructionMethodModelProtocol<NSObject>
@property (nonatomic, copy) NSString *method;
@property (nonatomic, assign) BOOL isStatic;
@property (nonatomic, assign) BOOL isMsgForwardStret;
@property (nonatomic, copy) NSArray<id<IPIntructionMessageModelProtocol>> *messages;
@end
@protocol IPIntructionClassModelProtocol<NSObject>
@property (nonatomic, copy) NSString *cls;
@property (nonatomic, copy) NSArray<id<IPIntructionMethodModelProtocol>> *methodList;
@end
@protocol IPIntructionModelProtocol<NSObject>
@property (nonatomic, copy) NSArray<id<IPIntructionClassModelProtocol>> *instructions;
@end
IPIntructionArgumentModelProtocol
這里有個(gè)innerMessage
膜赃,這是實(shí)現(xiàn)block
、if-else
揉忘、while
的關(guān)鍵跳座,在代碼實(shí)現(xiàn)上其實(shí)就是遞歸調(diào)用,借助環(huán)境池泣矛,使每次調(diào)用都能捕獲上一層的變量疲眷。
TODO
- 一個(gè)自動(dòng)生成json文件的腳本,提高代碼可讀性乳蓄,更接近OC
- 更友好方式去支持自定義Model
- 支持GCD
- 支持更多除了if-else咪橙、while的基本語法,并實(shí)現(xiàn)熱插拔
......
總結(jié)
大致思路說完了虚倒,單測(cè)測(cè)例美侦、技術(shù)細(xì)節(jié)處理在慢慢完善中,整體實(shí)現(xiàn)也非常簡(jiǎn)單魂奥。這里多說一句菠剩,熱修復(fù)已經(jīng)被蘋果一棒子打死了,現(xiàn)在在用的一些熱修復(fù)框架也是主要是靠繞過蘋果審核耻煤,但是我沒找到很好的介紹蘋果審核手段相關(guān)技術(shù)文章具壮,求!
開源地址:InstructionPatch