美文网首页iOS-Android-私房菜疯-iOS杂设计之美
iOS 面向切面编程的实现与实战案例

iOS 面向切面编程的实现与实战案例

作者: 景铭巴巴 | 来源:发表于2016-09-23 19:49 被阅读841次

    一、简介

    一、所谓的 aop 编程(面向切面编程),其原理也就是在不更改正常的业务处理流程的前提下,通过生成一个动态代理类,从而实现对目标对象嵌入附加的操作。在 iOS 中,要想实现相似的效果也很简单,利用 OC 的动态性,通过 Method Swizzling 改变目标函数的 selector 所指向的实现,然后在新的实现中实现附加的操作,完成之后再回到原来的处理逻辑。
    二、在一个类没有实现源码的情况下,如果你要改变一个类的实现方法,你可以选择重继承该类,然后重写方法,或者使用Category类别名暴力抢先的方式。但是这两种方式,都需要我们在使用的时候改变我们的编程方式,或者继承该类,或者需要引入Category。下面推出的一种方式,不需要我们修改我们编写逻辑的代码,就能实现函数的Hook功能,那就是RunTime中的Method Swizzling—交换方法的实现。

    二、实现原理

    在Object-C中每一个Method都是由一个SEL(方法名的散列值)和一个方法实现的指针(IMP)组成,他们在类实例化得过程中,SEL和IMP一一对应组成我们需要的完整的Method。

    struct method_t {
        SEL name;//方法名的散列值
        const char *types;//方法的描述
        IMP imp;//方法真实实现的指针
    };
    

    如果我们不做任何处理,SEL和IMP都是一一对应的。

    如果我们使用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);
    

    进入它的源码,可以查看它就是按照以上思路把方法指针做了交换,来做到在运行时把方法进行交换。

    下面就是它实现的关键源码。

    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的对应关系就如下所示了。

    三、核心代码

    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);//交换方法
            }
        }
    }
    

    第一个参数为:需要交换方法的类的名称。

    第二个参数为:原始方法名。

    第三个参数为:交换方法名。

    第四个参数为:交换方法的方法指针。

    具体每一段代码已经在主时钟说明的非常清楚了,就不多讲了。下面进入实战环节。

    四、页面埋点的实现

    如果我们要实现页面埋点的话,我们就需要在-(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
    

    这样我们需要修改我们原来的代码逻辑 就可以实现简单的埋点功能了。效果如下:

    五、网络图片信息监控工具

    在我们日常开发中,网络图片下载显示工具使用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);
        }
    }
    

    实现效果如下:

    六、注意点

    你要确保Method Swizzling的交换代码在APP的运行周期中只被调用一次。大部分情况都是在+(void)load方法中被调用。或者在APPDelegate中的- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 方法中被调用。

    七、联系方式

    新浪微博
    github
    简书首页

    欢迎加好友、一起交流。

    相关文章

      网友评论

      • 58cb860debdd:你好,在埋点那个地方,为何不直接写个方法,然后method_exchangeImplementations,将它与 viewWillAppear交换,而要用文中这种写起来很麻烦的方法呢?
        景铭巴巴:@XDKHAN 放在一个公用的地方就可以了
        58cb860debdd:@景铭巴巴 哦,明白了。那你一般把这个函数放在那里呢?pch,还是全局的一个方法文件。
        景铭巴巴:@XDKHAN 我的这个方法参数都是字符串,低侵入性!
      • bigParis:你要确保Method Swizzling的交换代码在APP的运行周期中只被调用一次, 如果需要二次hook会有什么问题吗?
        景铭巴巴:@bigParis 也可以多次hook。但是你要保证sel和imp的对应关系不要混乱 就可以。

      本文标题:iOS 面向切面编程的实现与实战案例

      本文链接:https://www.haomeiwen.com/subject/ghatyttx.html