iOS RSSwizzle中的swizzle原理

RSSwizzle是一個簡單的hook函數(shù)的第三方庫,它的使用跟傳統(tǒng)的hook方式比起來更加便捷澄惊,也更加安全。下面來分析它是怎么做到的富雅。

傳統(tǒng)的hook方法

實現(xiàn)

一般的掸驱,如果我們要viewDidLoad,我們需要寫如下的代碼:

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
      SEL originalSel = @selector(viewDidLoad);
      SEL swizzleSel = @selector(swizzle_viewDidLoad);
      Method originalMethod =
          class_getInstanceMethod([self class], originalSel);
      Method swizzleMethod = class_getInstanceMethod([self class], swizzleSel);
      1.
      BOOL didAddMethod = class_addMethod(
          [self class], originalSel, method_getImplementation(swizzleMethod),
          method_getTypeEncoding(swizzleMethod));
      if (didAddMethod) {
      2.
        class_replaceMethod([self class], swizzleSel,
                            method_getImplementation(originalMethod),
                            method_getTypeEncoding(originalMethod));
      } else {
      3.
        method_exchangeImplementations(originalMethod, swizzleMethod);
      }
    });
}

- (void)viewDidLoad {
    [super viewDidLoad];
}

- (void)swizzle_viewDidLoad {
    [self swizzle_viewDidLoad];
    NSLog(@"hook viewDidLoad");
}

如上所示,主要的邏輯都在load函數(shù)里面没佑,其核心思路就是交換viewDidLoad和`swizzle_viewDidLoad``的實現(xiàn)毕贼,下面簡單解析一下:

  1. 嘗試給當前類添加一個方法,該方法的方法名是viewDidLoad蛤奢,實現(xiàn)是swizzle_viewDidLoad的實現(xiàn)鬼癣,這么做的目的是為了確保當前類一定有viewDidLoad這個方法名(否則使用method_exchangeImplementations交換不會成功)
  2. 添加成功,則將原來的swizzle_viewDidLoad的實現(xiàn)替換換成viewDidLoad的實現(xiàn)
  3. 如果添加不成功啤贩,則交換兩個方法的實現(xiàn)

這樣之后待秃,只要viewDidLoad被調(diào)用,則會走到swizzle_viewDidLoad的實現(xiàn)上來痹屹,而swizzle_viewDidLoad調(diào)用自己則走回原來的viewDidLoad的實現(xiàn)锥余,從而實現(xiàn)了hook

不足之處

這樣寫,大部分情況都是可以實現(xiàn)hook的痢掠,但是還是有一些邊界情況沒有考慮進去驱犹,比如originalSel在本類和父類都沒有實現(xiàn)的情況,可以參考這篇文章足画。另外雄驹,沒有一個hook就會要多寫一個方法,寫法上也不是很好淹辞。還有就是医舆,hook的代碼一旦不是寫在load函數(shù)里面(一般不會出現(xiàn)這種情況),則還要考慮多線程的問題。

RSSwizzle

RSSwizzle能規(guī)避上述的問題蔬将,如果要hook一個函數(shù)爷速,不管在什么地方,只需要這么寫:

    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        RSSwizzleInstanceMethod([self class], @selector(viewDidLoad), RSSWReturnType(void), RSSWArguments(), RSSWReplacement({
            RSSWCallOriginal();
            NSLog(@"hook viewDidLoad");
        }), RSSwizzleModeAlways, NULL)
    });

其中RSSwizzleInstanceMethod就是交換方法的宏霞怀,除了要傳viewDidLoad之外惫东,還要傳入方法的返回參數(shù)和方法的參數(shù),在block里面毙石,就是替換的實現(xiàn)廉沮,其中RSSWCallOriginal是另一個宏,就是調(diào)用原來的方法徐矩。

可以看出滞时,這樣調(diào)用比原來的方式要簡潔多了。

RSSwizzle代碼實現(xiàn)

在RSSwizzle.h文件中滤灯,定義了兩個類坪稽,RSSwizzleInfo用于保存原函數(shù)的實現(xiàn),RSSwizzle則是swizzle的主要類鳞骤,其中有兩個方法

+(BOOL)swizzleInstanceMethod:(SEL)selector
                     inClass:(Class)classToSwizzle
               newImpFactory:(RSSwizzleImpFactoryBlock)factoryBlock
                        mode:(RSSwizzleMode)mode
                         key:(const void *)key;
                         
+(void)swizzleClassMethod:(SEL)selector
                  inClass:(Class)classToSwizzle
            newImpFactory:(RSSwizzleImpFactoryBlock)factoryBlock;

看函數(shù)名可以知道窒百,這個兩個方法一個是針對類方法的swizzle一個是針對實例方法的swizzle。先看一下針對類方法的實現(xiàn):

+(void)swizzleClassMethod:(SEL)selector
                  inClass:(Class)classToSwizzle
            newImpFactory:(RSSwizzleImpFactoryBlock)factoryBlock
{
    [self swizzleInstanceMethod:selector
                        inClass:object_getClass(classToSwizzle)
                  newImpFactory:factoryBlock
                           mode:RSSwizzleModeAlways
                            key:NULL];
}

可以看出弟孟,其實最后還是調(diào)用swizzleInstanceMethod贝咙,只是把該類對象的元類傳進去而已。swizzleInstanceMethod的代碼如下:

+(BOOL)swizzleInstanceMethod:(SEL)selector
                     inClass:(Class)classToSwizzle
               newImpFactory:(RSSwizzleImpFactoryBlock)factoryBlock
                        mode:(RSSwizzleMode)mode
                         key:(const void *)key
{
    NSAssert(!(NULL == key && RSSwizzleModeAlways != mode),
             @"Key may not be NULL if mode is not RSSwizzleModeAlways.");

    @synchronized(swizzledClassesDictionary()){
    
        if (key){
    1.
            NSSet *swizzledClasses = swizzledClassesForKey(key);
            if (mode == RSSwizzleModeOncePerClass) {
                if ([swizzledClasses containsObject:classToSwizzle]){
                    return NO;
                }
            }else if (mode == RSSwizzleModeOncePerClassAndSuperclasses){
                for (Class currentClass = classToSwizzle;
                     nil != currentClass;
                     currentClass = class_getSuperclass(currentClass))
                {
                    if ([swizzledClasses containsObject:currentClass]) {
                        return NO;
                    }
                }
            }
        }
    2.
        swizzle(classToSwizzle, selector, factoryBlock);
        
        if (key){
            [swizzledClassesForKey(key) addObject:classToSwizzle];
        }
    }
    
    return YES;
}
  1. RSSwizzleMode是一個枚舉拂募,用于決定這次hook是否能hook多次還是只能hook一次(或者是父類hook一次)庭猩,它會根據(jù)key對應的集合是否有當前要hook的類決定是否使用這次hook,通常在開發(fā)中mode會傳RSSwizzleModeAlways陈症,key會傳NULL蔼水,因此代碼會直接走到2這邊來。PS感覺這個功能
    比較雞肋录肯,如果別的模塊使用傳統(tǒng)的swizzle方法還是會hook住的趴腋,實在想不到應用場景。
  2. 這是swizzle的核心方法论咏,RSSwizzleImpFactoryBlock的定義如下:
typedef id (^RSSwizzleImpFactoryBlock)(RSSwizzleInfo *swizzleInfo);

swizzle方法的實現(xiàn)如下:

static void swizzle(Class classToSwizzle,
                    SEL selector,
                    RSSwizzleImpFactoryBlock factoryBlock)
{
1.
    Method method = class_getInstanceMethod(classToSwizzle, selector);
    
    NSCAssert(NULL != method,
              @"Selector %@ not found in %@ methods of class %@.",
              NSStringFromSelector(selector),
              class_isMetaClass(classToSwizzle) ? @"class" : @"instance",
              classToSwizzle);

2.
    NSCAssert(blockIsAnImpFactoryBlock(factoryBlock),
             @"Wrong type of implementation factory block.");
3.    
    __block OSSpinLock lock = OS_SPINLOCK_INIT;
    __block IMP originalIMP = NULL;
4.
    RSSWizzleImpProvider originalImpProvider = ^IMP{

        OSSpinLockLock(&lock);
        IMP imp = originalIMP;
        OSSpinLockUnlock(&lock);
        
        if (NULL == imp){
            Class superclass = class_getSuperclass(classToSwizzle);
            imp = method_getImplementation(class_getInstanceMethod(superclass,selector));
        }
        return imp;
    };
5.    
    RSSwizzleInfo *swizzleInfo = [RSSwizzleInfo new];
    swizzleInfo.selector = selector;
    swizzleInfo.impProviderBlock = originalImpProvider;
6.    
    id newIMPBlock = factoryBlock(swizzleInfo);
    
    const char *methodType = method_getTypeEncoding(method);
    
    NSCAssert(blockIsCompatibleWithMethodType(newIMPBlock,methodType),
             @"Block returned from factory is not compatible with method type.");
    
    IMP newIMP = imp_implementationWithBlock(newIMPBlock);
7.    
    OSSpinLockLock(&lock);
    originalIMP = class_replaceMethod(classToSwizzle, selector, newIMP, methodType);
    OSSpinLockUnlock(&lock);
}
  1. 獲取原方法的method
  2. 確保factoryBlock是否是RSSwizzleImpFactoryBlock优炬,如果傳的是id(NSObject *obj){}類型的block編譯器不會報錯,但在這里會進斷言厅贪,其實就是判斷二者的簽名是否一致蠢护,這里就不展開講了,有興趣可以看這篇文章
  3. originalIMP是原來方法的實現(xiàn)养涮,目前是NULL,后續(xù)會給它賦值葵硕,它可能是線程不安全的(因為引用它的block不知道會被哪個線程調(diào)用)眉抬,因此需要加鎖保護
  4. originalImpProvider就是返回一個原來方法的實現(xiàn),如果本類沒有懈凹,還會往父類那里找直到找到為止蜀变。這是因為7中的class_replaceMethod只會返回本類的實現(xiàn),不會再父類中查找
  5. 初始化RSSwizzleInfo介评,給它賦值
  6. 調(diào)用factoryBlock库北,拿到新的block,然后比較這個block跟原函數(shù)的函數(shù)簽名是否一致,否則進斷言威沫,最后用這個block初始化newIMP
  7. class_replaceMethod替換原來的實現(xiàn)贤惯,如果本類沒有這個方法洼专,會默認加上這個替換的方法棒掠,返回的originalIMP就是原來的實現(xiàn)

從6可以看出,傳入的factoryBlock的返回只能是一個block屁商,否則這邏輯走不通烟很,這個函數(shù)執(zhí)行完之后,調(diào)用原來的函數(shù)蜡镶,就執(zhí)行了newIMP

此外雾袱,如果想在替換的方法里面調(diào)用原來的函數(shù),我們就需要在RSSwizzleInfo那里拿到原來的實現(xiàn)了官还,主要函數(shù)如下:

-(RSSwizzleOriginalIMP)getOriginalImplementation{
    NSAssert(_impProviderBlock,nil);
    return (RSSwizzleOriginalIMP)_impProviderBlock();
}

不難看出芹橡,其實就是調(diào)用4中的block取得IMP而已

如果只是通過swizzleInstanceMethod這個函數(shù)來實現(xiàn)swizzle,那么我們必須要傳對RSSwizzleImpFactoryBlock望伦,否則會進入斷言林说,這樣調(diào)用還是挺麻煩的,但是它提供的宏解決了這一問題屯伞。

RSSwizzle宏實現(xiàn)

使用RSSwizzle進行hook的時候會使用到它的很多個宏腿箩,下面從它的參數(shù)開始說起:

RSSWReturnType

#define RSSWReturnType(type) type

這是一個返回值的宏,可以填寫可以看出這個宏其實什么都沒做劣摇,原樣返回了珠移,但是這樣寫增加了代碼的易讀性

RSSWArguments

#define RSSWArguments(arguments...) _RSSWArguments(arguments)
#define _RSSWArguments(arguments...) DEL, ##arguments

這是一個可以填多個參數(shù)的宏,...是多個參數(shù)的意思末融,arguments是這些參數(shù)的集合钧惧。

##在這里的意思是:如果沒有arguments,則刪掉##arguments和前面的逗號勾习,也就是說浓瞪,RSSWArguments()最后得到的是DEL,而所有的傳參前面都會插入DEL這個標志位,后續(xù)會移除這個標志位语卤,這樣做是為了規(guī)避沒有參數(shù)的時候有時預編譯會出現(xiàn)多余逗號的bug追逮。

RSSWReplacement

#define RSSWReplacement(code...) code

這個宏封裝替換的函數(shù)

RSSwizzleInstanceMethod

#define RSSwizzleInstanceMethod(classToSwizzle, \
                                selector, \
                                RSSWReturnType, \
                                RSSWArguments, \
                                RSSWReplacement, \
                                RSSwizzleMode, \
                                key) \
    _RSSwizzleInstanceMethod(classToSwizzle, \
                             selector, \
                             RSSWReturnType, \
                             _RSSWWrapArg(RSSWArguments), \
                             _RSSWWrapArg(RSSWReplacement), \
                             RSSwizzleMode, \
                             key)

#define _RSSwizzleInstanceMethod(classToSwizzle, \
                                 selector, \
                                 RSSWReturnType, \
                                 RSSWArguments, \
                                 RSSWReplacement, \
                                 RSSwizzleMode, \
                                 KEY) \
    [RSSwizzle \
     swizzleInstanceMethod:selector \
     inClass:[classToSwizzle class] \
     newImpFactory:^id(RSSwizzleInfo *swizzleInfo) { \
        RSSWReturnType (*originalImplementation_)(_RSSWDel3Arg(__unsafe_unretained id, \
                                                               SEL, \
                                                               RSSWArguments)); \
        SEL selector_ = selector; \
        return ^RSSWReturnType (_RSSWDel2Arg(__unsafe_unretained id self, \
                                             RSSWArguments)) \
        { \
            RSSWReplacement \
        }; \
     } \
     mode:RSSwizzleMode \
     key:KEY];
                             

這個是最主要的宏酪刀,它封裝了整個函數(shù)的調(diào)用,將其展開那就是:

    [RSSwizzle 
     swizzleInstanceMethod:selector 
     inClass:[classToSwizzle class] 
     newImpFactory:^id(RSSwizzleInfo *swizzleInfo) { 
     1.
        RSSWReturnType (*originalImplementation_)(_RSSWDel3Arg(__unsafe_unretained id, 
                                                               SEL, 
                                                               RSSWArguments)); 
     2.
        SEL selector_ = selector; 
    3.
        return ^RSSWReturnType (_RSSWDel2Arg(__unsafe_unretained id self, 
                                             RSSWArguments)) 
        { 
            RSSWReplacement 
        }; 
     } 
     mode:RSSwizzleMode 
     key:KEY];

前兩個參數(shù)都挺好懂钮孵,主要看最后一個參數(shù):

1.根據(jù)函數(shù)參數(shù)和返回值聲明一個名為originalImplementation_的block骂倘,改block的返回值是RSSWReturnType,也就是傳入的返回值巴席,參數(shù)值是id,SEL,和RSSWArguments中傳入的參數(shù)历涝,_RSSWDel3Arg的宏定義如下:

#define _RSSWDel3Arg(a1, a2, a3, args...) a1, a2, ##args

這是為了去掉第三個參數(shù),前面說過RSSWArguments會在參數(shù)前面插入一個DEL漾唉,就在這里去掉了

2.定義selector_等于selector荧库,這是原函數(shù)的方法編號

3.定義了一個block作返回,這個block返回值是RSSWReturnType赵刑,參數(shù)是self和RSSWArguments中傳入的參數(shù)分衫,這里_RSSWDel2Arg的意思跟_RSSWDel3Arg類似,都是除掉多余的DEL,這個block的內(nèi)容就是替換的函數(shù)般此,從上面的代碼分析中我們知道蚪战,這個block就是用來初始化newIMP的。

RSSWCallOriginal

在hook的時候很多時候都需要調(diào)用會之前的函數(shù)铐懊,這個時候就要調(diào)用RSSWCallOriginal這個宏了邀桑,其定義如下:

#define RSSWCallOriginal(arguments...) _RSSWCallOriginal(arguments)

#define _RSSWCallOriginal(arguments...) \
    ((__typeof(originalImplementation_))[swizzleInfo \
                                         getOriginalImplementation])(self, \
                                                                     selector_, \
                                                                     ##arguments)

可以看出,它是在swizzleInfo中拿到imp指針科乎,然后將其強制轉(zhuǎn)換為originalImplementation_這個block進行調(diào)用壁畸,這樣的好處由于originalImplementation_的傳參和返回值都由外界決定,因此如果傳的不對編譯器會報錯茅茂,在一定程度上避免在運行期間進入斷言捏萍。

有了這些宏之后,hook就變得方便多了

總結(jié)

RSSwizzle雖然是個輕量易用的庫玉吁,總共的代碼量不多照弥,但涉及到的知識還是挺多的,其中運行時和宏的相關(guān)的代碼更是非常的精妙进副,推薦大家使用这揣。

參考文獻

Method Swizzle實際使用的坑

RSSwizzle源碼解析

iOS 開發(fā) 高級:使用 宏定義macros (#,##影斑,…给赞,__VA_ARGS_)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市矫户,隨后出現(xiàn)的幾起案子片迅,更是在濱河造成了極大的恐慌,老刑警劉巖皆辽,帶你破解...
    沈念sama閱讀 206,839評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件柑蛇,死亡現(xiàn)場離奇詭異芥挣,居然都是意外死亡,警方通過查閱死者的電腦和手機耻台,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,543評論 2 382
  • 文/潘曉璐 我一進店門空免,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人盆耽,你說我怎么就攤上這事蹋砚。” “怎么了摄杂?”我有些...
    開封第一講書人閱讀 153,116評論 0 344
  • 文/不壞的土叔 我叫張陵坝咐,是天一觀的道長。 經(jīng)常有香客問我析恢,道長墨坚,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,371評論 1 279
  • 正文 為了忘掉前任氮昧,我火速辦了婚禮框杜,結(jié)果婚禮上浦楣,老公的妹妹穿的比我還像新娘袖肥。我一直安慰自己,他們只是感情好振劳,可當我...
    茶點故事閱讀 64,384評論 5 374
  • 文/花漫 我一把揭開白布椎组。 她就那樣靜靜地躺著,像睡著了一般历恐。 火紅的嫁衣襯著肌膚如雪寸癌。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,111評論 1 285
  • 那天弱贼,我揣著相機與錄音蒸苇,去河邊找鬼。 笑死吮旅,一個胖子當著我的面吹牛溪烤,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播庇勃,決...
    沈念sama閱讀 38,416評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼檬嘀,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了责嚷?” 一聲冷哼從身側(cè)響起鸳兽,我...
    開封第一講書人閱讀 37,053評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎罕拂,沒想到半個月后揍异,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體全陨,經(jīng)...
    沈念sama閱讀 43,558評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,007評論 2 325
  • 正文 我和宋清朗相戀三年衷掷,在試婚紗的時候發(fā)現(xiàn)自己被綠了烤镐。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,117評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡棍鳖,死狀恐怖炮叶,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情渡处,我是刑警寧澤镜悉,帶...
    沈念sama閱讀 33,756評論 4 324
  • 正文 年R本政府宣布,位于F島的核電站医瘫,受9級特大地震影響侣肄,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜醇份,卻給世界環(huán)境...
    茶點故事閱讀 39,324評論 3 307
  • 文/蒙蒙 一稼锅、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧僚纷,春花似錦矩距、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,315評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至痊臭,卻和暖如春哮肚,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背广匙。 一陣腳步聲響...
    開封第一講書人閱讀 31,539評論 1 262
  • 我被黑心中介騙來泰國打工允趟, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人鸦致。 一個月前我還...
    沈念sama閱讀 45,578評論 2 355
  • 正文 我出身青樓潮剪,卻偏偏與公主長得像,于是被迫代替她去往敵國和親蹋凝。 傳聞我的和親對象是個殘疾皇子鲁纠,可洞房花燭夜當晚...
    茶點故事閱讀 42,877評論 2 345

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