前言:
在处理完框架内存泄漏的问题后,见上篇: 讲述 Sagit.Framework 解决:双向引用导致的 IOS 内存泄漏(中)- IOS 不为人知的 Bug
发现业务代码有一个地方的内存没释放,原因很也简单:
在 block 里用到了 self,造成双向引用,然后就开始思考怎么处理这个问题.
常规则思维,就是改代码,block 不要用到 self,或只用 self 的弱引用.
只是框架这里特别,有一个特好用的系列,STLastXXX 系列,是用宏定义的,而且该宏指向了 self.
这么好用的 STLastXXXX 宏定义系列,我希望它可以不受限制的到处使用.
于是开始折腾之路:
折腾一:在代码中重新定义宏?
上面的代码,说白了就是用到了 self,我们看一下宏定义:
通常的做法,遇到 block,都伴随有 WeakSelf 这东东,像这样:
STWeakSelf 的默认定义:
说白了,宏定义就是编绎期的文字替换游戏,如果我在 block 里的第一行代码,重新定义 sagit 这个宏会怎样?
//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;
比如这样定义:
然后这么使用:
看来是我想多,编绎都过不去.
折腾二:将宏定义指向一个函数
比如这样定义:
#define sagit[Sagit share].Layout
然后跑到 Sagit 这个类里定义一个 UIView* Layout 属性,比如这样:
然后,就是在各个基类 (STController 或 STView) 初始化时,将自身的 self.baseView 赋值给它.
PS:baseView 是对 UIView 和 UIViewController 扩展的一个属性,都指向 View.
比如:
看起来有点效果,不过,要用这种方式,还得思考的更全面:
:架框中每个STView都是baseView.
:STView可以无限嵌套STView.
:因此:在STView被初时化时,设置它为baseView,加载完后,如果STView有父的STView,交还控制权,(这里就涉及到嵌套的控制流程,如果各个子View是异步加载,那就悲催了).
:没有继承自STView的情况呢,怎么拦截View的开始和结束呢?(Sagit已经慢慢弱化基类的功能,多数功能都是在原生的上做扩展,所以不用STView,很多功能也可以正常使用)
好吧,写这么多,就是说这个方法是可以的,但必须考虑的更仔细好多些!!!!
而且这个方法,只是避开了 self,self 仍然不允许被存在.
所以,这个方法,先暂放,看看有没有办法,打破 self 的循环引用先.
折腾三:思考如何打破 block 和 self 的双向引用?
block 和 self,互相的强引用,要打破它,总得有一方要示弱.
既然 block 中要允许 self 中存,就意味着 block 对 self 必然是强引用,辣么就只能思考,如果让 self 对 block 只能是弱引用了,或者没有引用!
先来看框架的一段代码:
被圈起来的代码,实现的下列图中圈起来的功能:
对于 Sagit 中,实现表格的代码是不是觉的很简单,不过教程还没写,这里提前泄漏了.
还是说重点,重点为 UITableView 中,扩展了一个属性 AddCell 的 Block 属性,见代码如下:
虽然定义了一个 addCell 属性,但是怎么持有,还得看 getter 和 setter 怎么实现:
@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 这个 block 存到和 UITableView 无关的地方去了?
@implementation UITableView(ST)
#pragma mark 核心扩展
-(AddTableCell)addCell
{
//从第三方持有返回
}
-(void)setAddCell:(AddTableCell)addCell
{
if(addCell!=nil)
{
//第三方持有addCell
}
else
{
}
}
用一个第三方持有 block,只要这个第三方不和和 self 发生直接关系,那么应该就不会有问题.
引用关系就变成:
第三方:强引用=》block
block:强引用=》self
这里的释放关系,第三方活着,就不会释放 block,block 活着,就不会释放 self.
所以结论:还是不释放.
也就是说,一顿代码写下来,虽然解除了关系,但还是没释放.
如果第三方对 block 是弱引用呢?
为了这个实验,我新建了一个项目,然后在这个项目上,试了整整 24 小时:
实验过程没有得到的想要的结果,但了解 block 的原理,和认清自己的逻辑误区.
实验的得到的知识后面再分享,这里先继续:
由于这里addCell,必须始终存活,因为你不知道什么时候,可能又要UITableView的reloadData一下.
所以,addCell这个block,必须被强引用,而且生命周期得和UITableView一致.
所以,要打破这个核心,还是得有第三方行为事件来触发破除关系,故事就转变成为:由self去移除第三方.
如果一定义要由某个事件来触发解除关系,那么第三方也没存在的必要了.
因为正常A和B互相引用无解,是指他们互相无解,但只要有第三者存在,对其中一个置nil就解了.
最后的最后,框架的代码是这样的:
仍然是用强引用来存档 block,这里有一个注意事项:
-(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 持久化,先 copy 一下,不然你会死的很惨.
好吧,让它们互相强引用吧,剩下的事,就是谁来当这个第三方,以及怎么解除这层关系!!!
于是,到导航栏后退事件中,拦截,并做销毁工作:
Sagit 框架为:每个 view 和 controller 扩展了 dispose 方法,里面清掉键值对,等于把 block 置为 nil,解除了关系.
@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]);
}
除了导航后退,还需要拦截多一个事件,就是 presentViewController 的事件跳转时,也需要检测并销毁.
做好这两步之后,以后就可以轻松的在 block 里写 self 了,爱引用就引用了,反正故事的结尾,都有一个第三者来收尾.
而且强引用有一个好处:
:再也用不上WeakSelf这种定义了.
:由于是强引用,就不用去管:里面还要套个StrongSelf,去避开多线程时,self可能被移除时带来的闪退问题.
最后:吐槽一个 IOS 的另一个坑,又是神秘的 dealloc 方法:
上一篇文章,讲到,如果对 UIView 扩展了 dealloc 这方法,引发的命案是:
导航栏:二次后退就闪退.
这一篇,又被我发现,如果对 UIViewController 扩展 dealloc 这个方法,引发的命案:
UIAlertView:当 alertViewStyle 设置为带文本框时就闪退.
给大伙上一个图,先把 dealloc 打开:
然后运行的效果:
坑吧,好在有上一次的经验,赶紧新建了一个项目,然后用代码排除法,最终还是排到 dealloc 这里来.
看到这文章的同学,你们又可以去忽悠同事了.
总结:
整体折腾完内存释放问题后,Sagit 框架也高效了很多,也许是错觉.
IT 连的创业的也在继续,欢迎大伙持续关注,谢谢!
来源: https://www.cnblogs.com/cyq1162/p/8229084.html