ObjectC Hook函數的實現與實戰

1、簡介

在一個類沒有實現源碼的狀況下,若是你要改變一個類的實現方法,你能夠選擇重繼承該類,而後重寫方法,或者使用Category類別名暴力搶先的方式。可是這兩種方式,都須要咱們在使用的時候改變咱們的編程方式,或者繼承該類,或者須要引入Category。下面推出的一種方式,不須要咱們修改咱們編寫邏輯的代碼,就能實現函數的Hook功能,那就是RunTime中的Method Swizzling—交換方法的實現。面試

這是一個個人iOS交流羣:624212887,羣文件自行下載,無論你是小白仍是大牛熱烈歡迎進羣 ,分享面試經驗,討論技術, 你們一塊兒交流學習成長!但願幫助開發者少走彎路。——點擊:加入編程

2、實現原理

在Object-C中每個Method都是由一個SEL(方法名的散列值)和一個方法實現的指針(IMP)組成,他們在類實例化得過程當中,SEL和IMP一一對應組成咱們須要的完整的Method。安全

struct method_t {
    SEL name;//方法名的散列值
    const char *types;//方法的描述
    IMP imp;//方法真實實現的指針
};
複製代碼

若是咱們不作任何處理,SEL和IMP都是一一對應的。bash

1.png
1.png

若是咱們使用Method Swizzling交換Method2和Method3的實現的時候,咱們只須要在運行時把IMP2和IMP3的指向地址作個交換就能夠了。其實咱們調用的就是RunTime中的網絡

*/
OBJC_EXPORT void method_exchangeImplementations(Method m1, Method m2) 
     __OSX_AVAILABLE_STARTING(__MAC_10_5, __IPHONE_2_0);
複製代碼

進入它的源碼,能夠查看它就是按照以上思路把方法指針作了交換,來作到在運行時把方法進行交換。app

下面就是它實現的關鍵源碼。函數

void method_exchangeImplementations(Method m1, Method m2)
{
    if (!m1  ||  !m2) return;

    rwlock_writer_t lock(runtimeLock);

    if (ignoreSelector(m1->name)  ||  ignoreSelector(m2->name)) {
        // Ignored methods stay ignored. Now they're both ignored. m1->imp = (IMP)&_objc_ignored_method; m2->imp = (IMP)&_objc_ignored_method; return; } IMP m1_imp = m1->imp; m1->imp = m2->imp; m2->imp = m1_imp; // RR/AWZ updates are slow because class is unknown // Cache updates are slow because class is unknown // fixme build list of classes whose Methods are known externally? flushCaches(nil); updateCustomRR_AWZ(nil, m1); updateCustomRR_AWZ(nil, m2); } 複製代碼

方法就換以後,SEL和IMP的對應關係就以下所示了。工具

2.png
2.png

3、核心代碼

void methodExchange(const char *className, const char *originalMethodName, const char *replacementMethodName, IMP imp) {
    Class cls = objc_getClass(className);//獲得指定類的類定義
    SEL oriSEL = sel_getUid(originalMethodName);//把originalMethodName註冊到RunTime系統中
    Method oriMethod = class_getInstanceMethod(cls, oriSEL);//獲取實例方法
    struct objc_method_description *desc = method_getDescription(oriMethod);//得到指定方法的描述
    if (desc->types) {
        SEL buSel = sel_registerName(replacementMethodName);//把replacementMethodName註冊到RunTime系統中

        if (class_addMethod(cls, buSel, imp, desc->types)) {//經過運行時,把方法動態添加到類中
            Method buMethod  = class_getInstanceMethod(cls, buSel);//獲取實例方法
            method_exchangeImplementations(oriMethod, buMethod);//交換方法
        }
    }
}
複製代碼

第一個參數爲:須要交換方法的類的名稱。性能

第二個參數爲:原始方法名。學習

第三個參數爲:交換方法名。

第四個參數爲:交換方法的方法指針。

具體每一段代碼已經在主時鐘說明的很是清楚了,就很少講了。下面進入實戰環節。

4、頁面埋點的實現

若是咱們要實現頁面埋點的話,咱們就須要在-(void)viewWillAppear:(BOOL)animated;方法中寫入咱們的埋點代碼,這樣實際上是很是不優雅的,須要咱們在每一個ViewController中的-(void)viewWillAppear:(BOOL)animated;都須要加入相似的埋點代碼。這個時候咱們就可使用Method Swizzling來HOOK住-(void)viewWillAppear:(BOOL)animated;方法來進行修改。

咱們新建一個UIViewController的分類,在其中進行方法的交換 , 關鍵代碼以下:。

@implementation UIViewController (Track)

+ (void)load{//+load會在類初始加載時調用
    //替換viewWillAppear:方法
    methodExchange("UIViewController", "viewWillAppear:", "hook_viewWillAppear:", (IMP)imp_processViewWillAppear);
}

//實現新的方法
static void imp_processViewWillAppear(id self, SEL cmd, BOOL animated){

    //先執行原來的方法
    SEL oriSel = sel_getUid("hook_viewWillAppear:");
    void (*hook_viewWillAppear)(id, SEL, BOOL) = (void (*)(id,SEL,BOOL))[UIViewController instanceMethodForSelector:oriSel];//函數指針
    hook_viewWillAppear(self,cmd,animated);

    //添加埋點
    NSLog(@"進入: %@", NSStringFromClass([self class]));
}

@end
複製代碼

這樣咱們須要修改咱們原來的代碼邏輯 就能夠實現簡單的埋點功能了。效果以下:

3.png
3.png

5、網絡圖片信息監控工具

在咱們平常開發中,網絡圖片下載顯示工具使用SDWebImage這個開源項目比較多。查看他的源碼發現它的核心處理代碼實際上是下面這段函數。

- (void)sd_setImageWithURL:(NSURL *)url placeholderImage:(UIImage *)placeholder options:(SDWebImageOptions)options progress:(SDWebImageDownloaderProgressBlock)progressBlock completed:(SDWebImageCompletionBlock)completedBlock
複製代碼

咱們調用一下方法就能實現網絡圖片的正常顯示,可是咱們還不能自動加上圖片的基本信息到圖片中顯示。

UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(100, 150, 150, 300)];
    [imageView sd_setImageWithURL:[NSURL URLWithString:@"http://c.hiphotos.baidu.com/image/pic/item/8d5494eef01f3a296716a9b49a25bc315d607ce9.jpg"]];
    [self.view addSubview:imageView];
複製代碼

這時咱們就須要Hook住該方法,修改它的實現。

#define originalMethod_setImageWithURL "sd_setImageWithURL:placeholderImage:options:progress:completed:"
#define replacementMethod_setImageWithURL "hook_sd_setImageWithURL:placeholderImage:options:progress:completed:"

+ (void)load;
{
    NSLog(@"開啓圖片監控");
    methodExchange("UIImageView", originalMethod_setImageWithURL, replacementMethod_setImageWithURL, (IMP)imp_processSetImageWithURL);
}

/**
 *  replacementMethod_processNeoHttpTaskFinish方法的實現
 */

static void imp_processSetImageWithURL(id self, SEL cmd, NSURL *url, UIImage *placeholder,
                                      SDWebImageOptions options, SDWebImageDownloaderProgressBlock progressBlock,  SDWebImageCompletionBlock completedBlock) {
    //  Run original
    SEL oriSel = sel_getUid(replacementMethod_setImageWithURL);

    BOOL (*setImageWithURLMethod)(id, SEL, NSURL *, UIImage *, SDWebImageOptions, SDWebImageDownloaderProgressBlock,  SDWebImageCompletionBlock) =
    (BOOL (*)(id, SEL, NSURL *, UIImage *, SDWebImageOptions, SDWebImageDownloaderProgressBlock,  SDWebImageCompletionBlock))[UIImageView instanceMethodForSelector : oriSel];

    if (Open_Monitor) {
        NSTimeInterval startTime = CFAbsoluteTimeGetCurrent();

        BOOL imageIsExsit = NO;
        if ([[SDImageCache sharedImageCache] imageFromMemoryCacheForKey:[[SDWebImageManager sharedManager] cacheKeyForURL:url]]) {
            imageIsExsit = YES;
        }
        __weak typeof(self) weafSelf = self;
        SDWebImageCompletionBlock replaceCompletedBlock = ^(UIImage *image, NSError *error, SDImageCacheType cacheType,NSURL *imageURL) {
            NSTimeInterval endTime = CFAbsoluteTimeGetCurrent();

            NSData *data = UIImageJPEGRepresentation(image, 1.0);
            if (!data && data.length <= 0) {
                data = UIImagePNGRepresentation(image);
            }

            NSString *string = [NSString stringWithFormat:@"url: %@ \nsize: %.2fX%.2f(px) \ndownTime: %fs \ndownSize: %luK", [url absoluteString], image.size.width, image.size.height, endTime - startTime, [data length] / 1024];


             ((UIImageView *)weafSelf).image = [ImageMonitorService drawText:string inImage:((UIImageView *)weafSelf).image atPoint:CGPointZero];

            if (completedBlock) {
                completedBlock(((UIImageView *)weafSelf).image, error, cacheType,imageURL);
            }
        };
        setImageWithURLMethod(self,  cmd, url, placeholder, options, progressBlock,  replaceCompletedBlock);
    }
    else {
        setImageWithURLMethod(self,  cmd, url, placeholder, options, progressBlock,  completedBlock);
    }
}
複製代碼

實現效果以下:

4.png
4.png

6、注意點

你要確保Method Swizzling的交換代碼在APP的運行週期中只被調用一次。大部分狀況都是在+(void)load方法中被調用。或者在APPDelegate中的- (BOOL)application:(UIApplication )application didFinishLaunchingWithOptions:(NSDictionary )launchOptions 方法中被調用。

7、最後說一點

這是一個個人iOS交流羣:624212887,羣文件自行下載,無論你是小白仍是大牛熱烈歡迎進羣 ,分享面試經驗,討論技術, 你們一塊兒交流學習成長!但願幫助開發者少走彎路。——點擊:加入

若是以爲對你還有些用,就關注小編+喜歡這一篇文章。你的支持是我繼續的動力。

下篇文章預告:iOS中保證線程安全的幾種方式與性能對比

文章來源於網絡,若有侵權,請聯繫小編刪除。

相關文章
相關標籤/搜索