前言
随心所欲的闭包给我们的代码变了简洁很多,尤其是使用Kotlin和Swift做coding的时候,但是闭包虽好,可要当心。当心什么呢?,当心内存泄露问题。那今天我们就来探讨下闭包造成的内存泄露<下面使用的例子都会用Swift来演示,Java,OC or KT的思路均差不多>
概念
何为内存泄漏(Memory Leak)
指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。
一般我们所说的内存泄漏是指堆内存的泄漏,堆内存是指程序从堆中分配的,大小任意的(内存块的大小可以在程序运行期决定),使用完成之后必须显示释放内存。应用程序一般使用malloc、realoc、new等函数从堆中分配到一块内存块,使用完成后,程序必须负责相应的释放。在C中使用free(),C++中delete、delete[]、free()。而Java中由于垃圾回收机制不用手动释放。
如果内存不能释放,这块内存就不能再次使用,我们就说这块内存泄漏了。
危害
内存泄漏的堆积,这会最终消耗尽系统所有的内存

举个例子:我们去饭店吃饭,正常的流程是开桌 -->吃饭-->结账走人-->服务员打扫桌子等待下一位客人使用。但是如果你吃完饭结账后一直赖着着不走,服务员就不会过来收拾桌子,那么这个桌子的内存就泄露了。
产生原因
罪魁祸首就是循环强引用。就像两个人打架一样,双方都让对象先放手,自己才回放手。

那在日常的coding中,那些情况会出现这样的循环引用呢:
class Person {
var name = ""
lazy var actionPrint : (String)->Void = {
print( "\(self.name) 进行活动:\($0)" )
}
init(_ name:String) { self.name = name }
func running(){
actionPrint("跑步了")
}
func eating(){
actionPrint("吃饭了")
}
deinit {
print("Person:\(name)销毁")
}
}
//MARK: ----------- 执行
var 张三:Person? = Person("张三")
张三?.running()
张三 = nil
//MARK: ----------- 输出
控制台输出: 张三 进行活动:跑步了
造成person对象不能被释放的原因:

二者循环引用的列子
class View {
lazy var subView = SubView()
init() {
subView.clickListener = {
print("我被点击了 ==> \(self)")
}
}
func clickSub(){ subView.click() }
deinit { print("A销毁") }
}
class SubView {
var clickListener:(()->Void)?
func click(){ clickListener?() }
deinit { print("B销毁") }
}
//MARK: ------------- 执行 -----------
var a:View? = View()
a?.clickSub()
a = nil
//MARK: ----------- 输出
控制台输出: 我被点击了 ==> __lldb_expr_86.View
造成View SubView对象不能被释放的原因:

正是类似这样的循环强引用,导致了内存不能释放! 那如何打破循环引用呢? 如果爱,请放手!
GC 与 ARC
每种语言都有自己的一套垃圾回收的机制。ARC(Automatic Reference Counting,自动引用计数)与GC(Garbage Collection,垃圾收集),“引用计数法”也算是一种GC策略。<回收的算法自行百度,我也不会>
我的理解: java中有个垃圾回收器(GC线程),在程序运行的期间,垃圾回收器会不断的去扫描堆中的对象是否无人使用。如果无人使用就会回收了。
Swift使用自动引用计数(ARC)来跟踪并管理应用使用的内存。大部分情况下,这意味着在Swift语言中,内存管理"仍然工作",不需要自己去考虑内存管理的事情。当实例不再被使用时,ARC会自动释放这些类的实例所占用的内存。
注意:Swift中 引用计数只应用在Class。结构体(Structure)和枚举类型是值类型,并非引用类型。其他语言自行百度
举个例子,我的理解就是 Java相当于雇佣了一名清洁阿姨,会在你程序运行的时候不定时的来打扫下你的内存空间,你也可以催催他(System.gc() ),但是他不一定来打扫,因为她的的优先级不高。 ARC更新雇佣了一名会计,他会把你每次申请的空间做记录,一旦发现没有人用了,就会回收掉。
每个语言都有自己的游戏规则,要想在规则下自由自在,就要遵循它。
Java:的四种引用更像是和那位清洁阿姨商量的策略,告诉他看到我的时候要怎么处理。
a) 强引用(StrongReference):JVM 宁可抛出 OOM ,也不会让 GC 回收具有强引用的对象;
b) 软引用(SoftReference):只有在内存空间不足时,才会被回的对象;
c) 弱引用(WeakReference):在 GC 时,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存;
d) 虚引用(PhantomReference):任何时候都可以被GC回收,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否存在该对象的虚引用,来了解这个对象是否将要被回收。可以用来作为GC回收Object的标志。

Swift: Weak(弱引用) or unowned(无主引用) ,这两个关键字标识都是告诉那位会计叫他不要记账,我就是用用。 具体的含义我就不搜了,谈谈我的看法,无主引用是不安全的,你必须要确定被unowned标识的对象 在调用的时候一定不为空,不然就会报错。
无论是 软引用,弱引用,虚引用都是比较绅士的,在用之前会询问系统引用实例是否还在。unowned会比较暴力点,不会询问系统直接使用。 那什么场景会用到?后面会触及到
打破循环前的共识
当然咱们也不能草木皆兵,探讨前 了解以下共识~~
1. 值传递 or 引用传递
只有引用传递才会造成内存泄露,有人说: 啊呀我有个Double类型的会不会泄露啊 ? 不可能哦
2. 成员属性
有人说: 我刚刚在方法里new了个 AAA的对象传给工具类了,会不会内存泄露啊? AAA的对象的作用域在方法结束后结束了,你觉得会泄露吗?
3. 逃逸闭包(escaping) or 非逃逸闭包(noescape)
逃逸闭包:当函数执行结束后,才去调用函数内部的闭包,叫做逃逸闭包
非逃逸闭包:当函数执行过程中,执行的函数内部的闭包,叫做非逃逸闭包
只有逃逸闭包才会造成内存泄露哦!!!
//像这种 用完一次就扔的 就不会泄露
func setupView(_ b:View, _ init:(View)->Void ) -> View {
b.setColor = UIColor.red
init(b)
return b
}
4. 被别人引用的对象才会阻碍该对象释放

5. 别被某些语言的闭包简洁语法搞混了
class B {
fun action(){ }
}
//Kotlin
val b = B()
inline fun test( init : B.() -> Unit) : B {
b.init()
return b
}
//MARK: ---------
test{ this.action() }
//Swift
let b = B()
func test( init : (B) -> Unit) : B {
init(b)
return b
}
//MARK: ---------
test{ $0.action() }
打破循环<以Swift为例,其他思路同>
在开始的第一个列子处理方法:
lazy var actionPrint : (String)->Void = { [unowned self] in
print( "\(self.name) 进行活动:\($0)" )
}
weak unowned关键字只能修饰引用类型
class View {
lazy var subView = SubView()
init() {
subView.clickListener = { [weak self] in
print("我被点击了 ==> \(self)")
}
}
func clickSub(){ subView.click() }
deinit { print("A销毁") }
}
or
class View {
weak var subView:SubView? = SubView()
init() {
subView?.clickListener = {
print("我被点击了 ==> \(self)")
}
}
func clickSub(){ subView?.click() }
deinit { print("A销毁") }
}
//MARK: 正确的打开方式是第一种,第二种subView还没有使用 就被释放了,虽然解除了循环引用,但是subView 是要在View中使用的。
unowned正确打开方式
unowned的使用是更人性化的一种提现,它不希望你干一些脱裤子放屁的事情。如果你确认闭包调用的时机一定在 捕获列表所有参数的生命周期内,就可以直接用unowned
举个例子

这个关注按钮的点击事件 我封装在一个工具内里面
class AttUtils {
func att(_ attBo:MatchBo, _ attSuccCallBack:()->Void){
//把实体isAtt = true 不是网络请求
attSuccCallBack()
}
}
class 列表界面<BO,CELL:UITableViewCell> {
lazy var util = AttUtils()
....
func cellBindingData(_ b:BO,_ cell:CELL){
util(b){ [unowned self] in
cell.这个按钮变成已关注
self.弹框提示
}
}
....
}
weak 为什么不给AttUtils,因为 这个工具类 我要用。 为什么要用unowned,当界面已经被销毁后,这个按钮一定不可能被点击,所以这个闭包的self一定不会为空。
其他实际情况使用
网络请求的回调,一定使用weak,因为网络的周期,会在context生命周期之外
userDao.register(mobile, passwordField.text!, codeField.text!,handler).post{ [weak self] _ in
guard let strongSelf = self else { return }
strongSelf.routeConfig("registerSuccess").go()
}.excute()
Rx系列 用在view层 都是无主引用
let pwEvent = passwordField.rx.text.orEmpty
let codeEvent = codeField.rx.text.orEmpty
pwEvent.map { $0.count != 0 && $0.count < 6 }.bind{ [unowned self] in self.passwordField.isErrorRevealed = $0 }.disposed(by: disposeBag)
Observable.combineLatest(codeEvent,pwEvent).map{ [unowned self] in
$0.0.count > 0 && $0.1.count > 0 && !self.passwordField.isErrorRevealed
}.bind{ [unowned self] in self.setEnable(self.registerBtn,$0) }.disposed(by: disposeBag)
GCD 因为看不到源码
//这种属于必须要执行的 可以不加 实验结果是 执行完后 断开闭包的引用
DispatchQueue.main.async { self.action() }
DispatchQueue.global().async { print("=====>\(self.name)") }
Timer.scheduledTimer(withTimeInterval: 0, repeats: false) {
print("Timer=====>\(self.name) \($0)")
}
//一直循环运行的 不属于循环引用 属于霸占着一直不放手
Timer.scheduledTimer(withTimeInterval: 10, repeats: true) { [weak self] in
print("Timer=====>\(self?.name) \($0)")
}
图片下载 使用weak
self.kf.setImage(with: sUrl, placeholder: defPlaceHoder, options: [KingfisherOptionsInfoItem.downloader(ImageDownloader.default),KingfisherOptionsInfoItem.originalCache(ImageCache.default),KingfisherOptionsInfoItem.transition(ImageTransition.fade(2)),KingfisherOptionsInfoItem.callbackDispatchQueue(dispatchQueue),KingfisherOptionsInfoItem.processor(process)], progressBlock: nil){ [weak self] (imageTemp, error , cacheType ,imageURL ) in
self?.showToast("图片下载成功!")
}
其他
当然除了闭包可能造成内存泄露外,还有其他的方式也可能造成。
例如:
- 不要让我们的对象被静态属性 或者 file层级的属性 所引用,这很容易造成内存泄漏。
- 注册类的工具,记得要注销,例如 通知 广播 Eventbus。
- 循环执行的计时器 或者线程,记得在合适的时候手动停止。
内存分析工具
无论是studio还是xcode都有自己的内存分析工具,当然也有第三方库来监听内存使用情况(自行百度哈),避免内存泄露还得从coding中做起,希望今天的分享能帮助到你。
网友评论