美文网首页
iOS开发 Runtime 使用objc_msgSend()

iOS开发 Runtime 使用objc_msgSend()

作者: RITL | 来源:发表于2017-01-13 17:16 被阅读330次

    好几个月没有发表点东西了,但也一直关注简书上优秀的博文学习,漫长的这段时间一直在不断的重构公司项目,为了使代码尽可能的优雅,除了不断地进行组件化重写之外,很多地方也逐渐的开始使用runtime进行编程。说实话,这样编程比较爽,却会出现一系列之前没有考虑过的问题,比如这篇文章记录的使用objc_msgSend()创建对象不能释放的问题。

    先列出常用的初始化方法,这里就以最常用的ViewController为例。

    普遍的初始化

    UIViewController * viewController = [[UIViewController alloc]init];
    
    //或者
    
    UIViewController * viewController = [UIViewController new];
    

    楼主比较喜欢后面一种,因为毕竟能少打几个字母嘛,当然这不是重点。

    runtime的初始化

    Class class = objc_getClass("UIViewController");
    
    id viewController = (id(*)(id,SEL)objc_msgSend)(class,NSSelectorFromString(@"new"));
    

    两者对比一下,貌似使用runtime要更加的麻烦,因为它甚至需要两行比较长代码来完成一个控制器的初始化,至于为什么选择runtime后面会说一下个人的看法,如果非要说装X - - (我也说不了什么其实😀)。

    问题

    先来说明一下问题,利用Demo中的Let's push oneSelf来不断的push与pop当前控制器类的实例对象, 查看当前实例对象的个数,这个很简单,在类中定义一个全局变量来记录当前类对象实力对象的个数

    static NSUInteger classValue = 0;
    

    RITLRootViewController控制器的ViewDidLoad以及deallco方法中分别对数字进行++或者--操作,并打印当前类的个数:

    使用普通的初始化方法以及打印结果:

    RITLRootViewController * viewController = [RITLRootViewController new];
    [self.navigationController pushViewController:viewController animated:true];
    
    //result:
    现在存在1个RITLRootViewController
    现在存在2个RITLRootViewController
    现在存在1个RITLRootViewController
    现在存在2个RITLRootViewController
    现在存在1个RITLRootViewController
    现在存在2个RITLRootViewController
    现在存在1个RITLRootViewController
    

    可以看出,数字在viewDidLoad中进行了累加,在dealloc中进行了累减,这样是平衡的。

    使用runtime方法初始化打出结果:

    Class class = objc_getClass("RITLRootViewController");
    id viewController = ((id(*)(id,SEL))objc_msgSend)(class,NSSelectorFromString(@"new"));
    [self.navigationController pushViewController:viewController animated:true];
    
    //result:
    现在存在1个RITLRootViewController
    现在存在2个RITLRootViewController
    现在存在3个RITLRootViewController
    现在存在4个RITLRootViewController
    现在存在5个RITLRootViewController
    

    对象的个数是不断上升的,间接地告诉我们实例对象是没有释放掉的。

    原因

    用群里一个朋友的话说:"C语言的方式还要用C语言来解决"。

    runtime大家都知道它就是C/C++ , 再直接一点就是直接使用runtime执行初始化方法创建的对象的时候是不在ARC控制之下的,也就是说在对应创建的那个文件里销毁的时候需要我们自行release。

    楼主分析的小过程,很简单:

    • 初始化完毕之后,当前对象的retainCount = 1。
    • 导航控制器push的时候,ARC下的编译器帮我们进行了一次retain,当前对象的retainCount = 2。
    • 导航控制器pop的时候,ARC下的编译器又帮我们进行了一次release,当前对象的retainCount = 1。
    • 此时当前控制器的retainCount恒为非0正整数,无法释放。

    解决

    上面的步骤看完之后,基本就能进行解决位置的定位了,那么我们在导航控制器retain之后进行自身的release即可,ARC下由于不能使用release方法,解决办法如下:

    // 由于导航控制器持有viewController,所以viewController不会释放
    [self.navigationController pushViewController:viewController animated:true];
        
    //release,这个时候viewController的retainCount = 1,也就是在导航控制器释放的时候viewController也就会跟着进行释放
    ((void(*)(id,SEL))objc_msgSend)(viewController,NSSelectorFromString(@"release"));
    

    楼主的使用场景

    说明一下楼主使用runtime的场景之一,既然问题那么多,并且代码这么麻烦,为什么还要用它呢,难道就是为了装X吗,实际上并不是这样的。

    比如一个viewController需要跳转不同的控制器,由于楼主比较喜欢使用MVVM模型,每次处理完毕数据就要进行一个回调,跳入不同的控制器,如果有跳入100个不同的控制器的可能,那我岂不是要写N个回调,但是如果是使用runtime,我貌似只使用一个或者根据情况使用几个回调,返回控制器的类名以及相应的参数就好了吧。

    下面是一个逻辑十分简单的Demo,有三个不同的按钮,分别表示跳入三个不同的控制器,每点击一个按钮都会触发ViewModel的方法(MVVM模型的数据是在ViewModel中的呢,ViewController只是作为一个View层,不会有任何的逻辑),由viewModel通过某些方法进行数据处理完毕后进行界面的回调。

    简单的效果.git

    控制器的viewModel很简单,如下:

    RITLRootViewModel.h
    
    NS_ASSUME_NONNULL_BEGIN
    
    /// RITLRootViewController的viewModel对象
    @interface RITLRootViewModel : NSObject
    
    ///buttonDidTapWithTag:触发的block
    @property (nonatomic, copy, nullable)void(^ButtonDidTapBlock)(NSString * controllerName);
    
    /**
     根据不同的tag进行响应不同的事件,触发ButtonDidTapBlock
    
     @param tag button的tag值
     */
    - (void)buttonDidTapWithTag:(NSUInteger)tag;
    
    @end
    
    NS_ASSUME_NONNULL_END
    
    RITLRootViewModel.m
    
    @interface RITLRootViewModel ()
    
    @property (nonatomic, copy)NSArray < NSString * > * controllerNames;
    
    @end
    
    @implementation RITLRootViewModel
    
    -(instancetype)init
    {
        if (self = [super init])
        {
            _controllerNames = @[@"RITLRootViewController",@"RITLViewControllerTwo",@"RITLViewControllerThree"];
        }
        
        return self;
    }
    
    
    -(void)buttonDidTapWithTag:(NSUInteger)tag
    {
        NSUInteger realTag = tag - 10001;
        
        if (self.ButtonDidTapBlock)
        {
            self.ButtonDidTapBlock(self.controllerNames[realTag]);
        }
    }
    
    @end
    

    只需要在button点击的时候调用viewModel的buttonDidTapWithTag:方法即可:

    - (IBAction)pushBtnDidTap:(id)sender
    {
          UIButton * button = (UIButton *)sender;
          [self.viewModel buttonDidTapWithTag:button.tag];
     }
    

    在viewDidLoad绑定好viewModel即可

    - (void)bindViewModel
    {
        __weak typeof(self) weakSelf = self;
        
        self.viewModel.ButtonDidTapBlock = ^(NSString * controllerName){
            
            __strong typeof(weakSelf) strongSelf = weakSelf;
            
            Class class = objc_getClass(controllerName.UTF8String);
            
            id viewController = ((id(*)(id,SEL))objc_msgSend)(class,NSSelectorFromString(@"new"));
    
            [strongSelf ritl_pushViewController:viewController];
        };
    }
    
    // 导航控制器获得控制权后进行release即可
    - (void)ritl_pushViewController:(__kindof UIViewController *)viewController
    {
        [self pushViewController:viewController];
        
        //release
        ((void(*)(id,SEL))objc_msgSend)(viewController,NSSelectorFromString(@"release"));
    }
    

    补充

    这里附上此博文中的小demo,如果喜欢请Star支持一下 传送门

    我知道很多人提到MVVM首先会想到ReactiveCocoa或者RxSwfit,个人看来,它们只是帮助我们更优雅地实现MVVM模型,并不代表MVVM必须使用它们,毕竟MVVM是一种架构,一种思想。

    快到新年了,楼主还要接着对项目进行整改,也不知道会不会还有博文了,提前祝大家新年快乐啊。😀

    相关文章

      网友评论

          本文标题:iOS开发 Runtime 使用objc_msgSend()

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