iOS Hotfix,老思路新框架

背景

目前大多數(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)清空。

不依賴其他語言引擎好處有:

  1. 不需要引入多余的引擎
  2. 支持的系統(tǒng)版本更多
  3. 可控性強(qiáng)榛丢,無論是對(duì)象轉(zhuǎn)換還是引用管理铲球,當(dāng)然做的也多

壞處:

  1. 基本語法不支持,諸如if-else晰赞、while也需要自己實(shí)現(xiàn)
  2. 熱修復(fù)代碼可讀性差稼病,當(dāng)然可以通過腳本自動(dòng)生成json來優(yōu)化
  3. 修復(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)造,這樣問題就變成了:

  1. 如何調(diào)用參數(shù)傲诵、返回值不確定的block變量
  2. 如何構(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)blockif-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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末哈蝇,一起剝皮案震驚了整個(gè)濱河市棺妓,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌炮赦,老刑警劉巖怜跑,帶你破解...
    沈念sama閱讀 210,914評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異吠勘,居然都是意外死亡性芬,警方通過查閱死者的電腦和手機(jī)峡眶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評(píng)論 2 383
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來植锉,“玉大人辫樱,你說我怎么就攤上這事】”樱” “怎么了狮暑?”我有些...
    開封第一講書人閱讀 156,531評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長暇赤。 經(jīng)常有香客問我心例,道長宵凌,這世上最難降的妖魔是什么鞋囊? 我笑而不...
    開封第一講書人閱讀 56,309評(píng)論 1 282
  • 正文 為了忘掉前任,我火速辦了婚禮瞎惫,結(jié)果婚禮上溜腐,老公的妹妹穿的比我還像新娘。我一直安慰自己瓜喇,他們只是感情好挺益,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,381評(píng)論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著乘寒,像睡著了一般望众。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上伞辛,一...
    開封第一講書人閱讀 49,730評(píng)論 1 289
  • 那天烂翰,我揣著相機(jī)與錄音,去河邊找鬼蚤氏。 笑死甘耿,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的竿滨。 我是一名探鬼主播佳恬,決...
    沈念sama閱讀 38,882評(píng)論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼于游!你這毒婦竟也來了毁葱?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,643評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤贰剥,失蹤者是張志新(化名)和其女友劉穎倾剿,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體鸠澈,經(jīng)...
    沈念sama閱讀 44,095評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡柱告,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,448評(píng)論 2 325
  • 正文 我和宋清朗相戀三年截驮,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片际度。...
    茶點(diǎn)故事閱讀 38,566評(píng)論 1 339
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡葵袭,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出乖菱,到底是詐尸還是另有隱情坡锡,我是刑警寧澤,帶...
    沈念sama閱讀 34,253評(píng)論 4 328
  • 正文 年R本政府宣布窒所,位于F島的核電站鹉勒,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏吵取。R本人自食惡果不足惜禽额,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,829評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望皮官。 院中可真熱鬧脯倒,春花似錦、人聲如沸捺氢。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽摄乒。三九已至悠反,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間馍佑,已是汗流浹背斋否。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評(píng)論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留挤茄,地道東北人如叼。 一個(gè)月前我還...
    沈念sama閱讀 46,248評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像穷劈,于是被迫代替她去往敵國和親笼恰。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,440評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容

  • 《Objective-C高級(jí)編程》這本書就講了三個(gè)東西:自動(dòng)引用計(jì)數(shù)歇终、block社证、GCD,偏向于從原理上對(duì)這些內(nèi)容...
    WeiHing閱讀 9,804評(píng)論 10 69
  • *面試心聲:其實(shí)這些題本人都沒怎么背,但是在上海 兩周半 面了大約10家 收到差不多3個(gè)offer,總結(jié)起來就是把...
    Dove_iOS閱讀 27,128評(píng)論 29 470
  • 相逢是一場(chǎng)春夢(mèng) 重逢才值得歌頌 感謝相逢時(shí) 我們還什么都不懂 但愿重逢時(shí) 我們能笑談命運(yùn)的作弄
    段童閱讀 306評(píng)論 0 3
  • 想必大家都知道评凝,昭通大山包追葡,灑漁河邊的煙柳,還有望海公園的望海樓……都是著名的旅游景點(diǎn),但這些地方的冬天才是最美...
    臧宇光閱讀 720評(píng)論 2 1
  • 當(dāng)時(shí)覺得高三只有枯燥無聊的三點(diǎn)一線 回憶起來 卻是哭哭笑笑最難忘的一年 高考結(jié)束了。沈一點(diǎn)的團(tuán)隊(duì)白天蹲在考場(chǎng)外采訪...
    蘇蘇蘇三人行閱讀 570評(píng)論 0 1