• 讲述Sagit.Framework解决:双向引用导致的IOS内存泄漏(下)- block中任性用self


    前言:

    在处理完框架内存泄漏的问题后,见上篇:讲述Sagit.Framework解决:双向引用导致的IOS内存泄漏(中)- IOS不为人知的Bug

    发现业务代码有一个地方的内存没释放,原因很也简单:

    在block里用到了self,造成双向引用,然后就开始思考怎么处理这个问题。

    常规则思维,就是改代码,block不要用到self,或只用self的弱引用。

    只是框架这里特别,有一个特好用的系列,STLastXXX系列,是用宏定义的,而且该宏指向了self。

    这么好用的STLastXXXX宏定义系列,我希望它可以不受限制的到处使用。

    于是开始折腾之路:

    折腾一:在代码中重新定义宏?

    上面的代码,说白了就是用到了self,我们看一下宏定义:

    通常的做法,遇到block,都伴随有WeakSelf这东东,像这样:

    STWeakSelf的默认定义:

    //block块中用的引用
    #define STWeakSelf __weak typeof(self) selfWeak = self;__weak typeof(selfWeak) this = selfWeak;
    #define STWeakObj(o) __weak typeof(o) o##Weak = o;
    #define STStrongObj(o) __strong typeof(o) o = o##Weak;

    说白了,宏定义就是编绎期的文字替换游戏,如果我在block里的第一行代码,重新定义sagit这个宏会怎样? 

    比如这样定义:

    然后这么使用:

    看来是我想多,编绎都过不去。

    折腾二:将宏定义指向一个函数

    比如这样定义:

    #define sagit [Sagit share].Layout

    然后跑到Sagit这个类里定义一个UIView* Layout属性,比如这样:

    然后,就是在各个基类(STController或STView)初始化时,将自身的self.baseView赋值给它。

    PS:baseView是对UIView和UIViewController扩展的一个属性,都指向View。

    比如:

    看起来有点效果,不过,要用这种方式,还得思考的更全面:

    1:架框中每个STView都是baseView。
    
    2:STView可以无限嵌套STView。
    
    3:因此:在STView被初时化时,设置它为baseView,加载完后,如果STView有父的STView,交还控制权,(这里就涉及到嵌套的控制流程,如果各个子View是异步加载,那就悲催了)。
    
    4:没有继承自STView的情况呢,怎么拦截View的开始和结束呢?(Sagit已经慢慢弱化基类的功能,多数功能都是在原生的上做扩展,所以不用STView,很多功能也可以正常使用)

    好吧,写这么多,就是说这个方法是可以的,但必须考虑的更仔细好多些!!!!

    而且这个方法,只是避开了self,self仍然不允许被存在。

    所以,这个方法,先暂放,看看有没有办法,打破self的循环引用先。

    折腾三:思考如何打破block和self的双向引用?

     block和self,互相的强引用,要打破它,总得有一方要示弱。

    既然block中要允许self中存,就意味着block对self必然是强引用,辣么就只能思考,如果让self对block只能是弱引用了,或者没有引用!

    先来看框架的一段代码:

    被圈起来的代码,实现的下列图中圈起来的功能:

    对于Sagit中,实现表格的代码是不是觉的很简单,不过教程还没写,这里提前泄漏了。

    还是说重点,重点为UITableView中,扩展了一个属性AddCell的Block属性,见代码如下:

    @interface UITableView(ST)
    
    #pragma mark 核心扩展
    typedef void(^AddTableCell)(UITableViewCell *cell,NSIndexPath *indexPath);
    typedef BOOL(^DelTableCell)(UITableViewCell *cell,NSIndexPath *indexPath);
    typedef void(^AfterTableReloadData)(UITableView *tableView);
    //!用于为Table追加每一行的Cell
    @property (nonatomic,copy) AddTableCell addCell;
    //!用于为Table移除行的Cell
    @property (nonatomic,copy) DelTableCell delCell;
    //!用于为Table reloadData 加载完数据后触发
    @property (nonatomic,copy) AfterTableReloadData afterReload;
    //!获取Table的数据源
    @property (nonatomic,strong) NSMutableArray<id> *source;
    //!设置Table的数据源
    -(UITableView*)source:(NSMutableArray<id> *)dataSource;

    虽然定义了一个addCell属性,但是怎么持有,还得看getter和setter怎么实现:

    @implementation UITableView(ST)
    
    #pragma mark 核心扩展
    
    -(AddTableCell)addCell
    {
        //从第三方持有返回
    }
    -(void)setAddCell:(AddTableCell)addCell
    {
        if(addCell!=nil)
        {
             //第三方持有addCell
        }
        else
        {
            
        }
    }

    如果这里,把存档addCell这个block存到和UITableView无关的地方去了?

    用一个第三方持有block,只要这个第三方不和和self发生直接关系,那么应该就不会有问题。

    引用关系就变成:

    第三方:强引用=》block
    
    block:强引用=》self

    这里的释放关系,第三方活着,就不会释放block,block活着,就不会释放self。

    所以结论:还是不释放。

    也就是说,一顿代码写下来,虽然解除了关系,但还是没释放。

    如果第三方对block是弱引用呢?

    为了这个实验,我新建了一个项目,然后在这个项目上,试了整整24小时:

    实验过程没有得到的想要的结果,但了解block的原理,和认清自己的逻辑误区。

    实验的得到的知识后面再分享,这里先继续:

    由于这里addCell,必须始终存活,因为你不知道什么时候,可能又要UITableView的reloadData一下。
    
    所以,addCell这个block,必须被强引用,而且生命周期得和UITableView一致。
    
    所以,要打破这个核心,还是得有第三方行为事件来触发破除关系,故事就转变成为:由self去移除第三方。
    
    如果一定义要由某个事件来触发解除关系,那么第三方也没存在的必要了。
    
    因为正常A和B互相引用无解,是指他们互相无解,但只要有第三者存在,对其中一个置nil就解了。

    最后的最后,框架的代码是这样的:

    -(AddTableCell)addCell
    {
        return [self key:@"addCell"];
    }
    -(void)setAddCell:(AddTableCell)addCell
    {
        if(addCell!=nil)
        {
            addCell=[addCell copy];
            [self key:@"addCell" value:addCell];
        }
        else
        {
            [self.keyValue remove:@"addCell"];
        }
    }

    仍然是用强引用来存档block,这里有一个注意事项:

    如果你想将一个block持久化,先copy一下,不然你会死的很惨。

    好吧,让它们互相强引用吧,剩下的事,就是谁来当这个第三方,以及怎么解除这层关系!!!

    于是,到导航栏后退事件中,拦截,并做销毁工作:

    @implementation UINavigationController (ST)
    
    #pragma mark NavigationBar 的协议,这里触发
    // fuck shouldPopItem 方法存在时,只会触发导航栏后退,界面视图却不后退。
    //- (BOOL)navigationBar:(UINavigationBar *)navigationBar shouldPopItem:(UINavigationItem *)item  // same as push methods
    //{
    ////    //重设上一个Controller的导航(不然在二次Push后再Pop会Crash)
    ////    NSInteger count=self.viewControllers.count;
    ////    if(count>0)//发现这里返回的viewControllers,已经是移掉了当前的Controller后剩下的。
    ////    {
    ////        UIViewController *preController=self.viewControllers[count-1];//获取上一个控制器
    ////        if([preController needNavBar])
    ////        {
    ////            [preController reSetNav:self];
    ////        }
    ////    }
    ////
    //    return YES;
    //}
    //返回到当前页面
    - (void)navigationBar:(UINavigationBar *)navigationBar didPopItem:(UINavigationItem *)item
    {
    //    if(navigationBar!=nil && [navigationBar.lastSubView isKindOfClass:[UIButton class]])
    //    {
    //       // [navigationBar.lastSubView height:0];//取消自定义复盖的UIButton
    //    }
        NSInteger count=self.viewControllers.count;
        if(count>0)
        {
            UIViewController *current=self.viewControllers[count-1];
            self.navigationBar.hidden=![current needNavBar];
            if(self.tabBarController!=nil)
            {
                self.tabBarController.tabBar.hidden=![current needTabBar];
            }
            //检测上一个控制器有没有释放
            UIViewController *nextController=current.nextController;
            if(nextController!=nil)
            {
                [nextController dispose];
                nextController=nil;
            }
        }
    }
    -(void)dealloc
    {
        NSLog(@"UINavigationController relase -> %@", [self class]);
    }

    Sagit框架为:每个view和controller扩展了dispose方法,里面清掉键值对,等于把block置为nil,解除了关系。

    除了导航后退,还需要拦截多一个事件,就是presentViewController的事件跳转时,也需要检测并销毁。

    做好这两步之后,以后就可以轻松的在block里写self了,爱引用就引用了,反正故事的结尾,都有一个第三者来收尾

    而且强引用有一个好处:

    1:再也用不上WeakSelf这种定义了。
    
    2:由于是强引用,就不用去管:里面还要套个StrongSelf,去避开多线程时,self可能被移除时带来的闪退问题。

    最后:吐槽一个IOS的另一个坑,又是神秘的dealloc方法:

    上一篇文章,讲到,如果对UIView扩展了dealloc这方法,引发的命案是:

    导航栏:二次后退就闪退。

    这一篇,又被我发现,如果对UIViewController扩展dealloc这个方法,引发的命案:

    UIAlertView:当alertViewStyle设置为带文本框时就闪退。

    给大伙上一个图,先把dealloc打开:

    然后运行的效果:

    坑吧,好在有上一次的经验,赶紧新建了一个项目,然后用代码排除法,最终还是排到dealloc这里来。

    看到这文章的同学,你们又可以去忽悠同事了。

    总结:

    整体折腾完内存释放问题后,Sagit框架也高效了很多,也许是错觉。

    IT连的创业的也在继续,欢迎大伙持续关注,谢谢!

  • 相关阅读:
    C# 加载 SQLite DLL问题
    Linux chroot 并使用之前系统设备节点
    I.MX6 initramfs.cpio.gz.uboot unpack
    I.MX6 eMMC 添加分区
    Git 一次性 pull push 所有的分支
    ARM compiler No such file or directory
    Linux sed 替换第一次出现的字符串
    C# WinForm 应用程序 开启Console窗口
    No 'Access-Control-Allow-Origin' header is present on the requested resource.
    C# 集合已修改;可能无法执行枚举操作
  • 原文地址:https://www.cnblogs.com/cyq1162/p/8229084.html
Copyright © 2020-2023  润新知