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中保证线程安全的几种方式与性能对比

文章来源于网络,若有侵权,请联系小编删除。

相关文章
相关标签/搜索