美文网首页
移动端探讨闭包的内存泄露(公司分享)

移动端探讨闭包的内存泄露(公司分享)

作者: 747a945a4501 | 来源:发表于2018-07-12 17:45 被阅读93次

前言


随心所欲的闭包给我们的代码变了简洁很多,尤其是使用Kotlin和Swift做coding的时候,但是闭包虽好,可要当心。当心什么呢?,当心内存泄露问题。那今天我们就来探讨下闭包造成的内存泄露<下面使用的例子都会用Swift来演示,Java,OC or KT的思路均差不多>

概念


何为内存泄漏(Memory Leak)
指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。
一般我们所说的内存泄漏是指堆内存的泄漏,堆内存是指程序从堆中分配的,大小任意的(内存块的大小可以在程序运行期决定),使用完成之后必须显示释放内存。应用程序一般使用malloc、realoc、new等函数从堆中分配到一块内存块,使用完成后,程序必须负责相应的释放。在C中使用free(),C++中delete、delete[]、free()。而Java中由于垃圾回收机制不用手动释放。
如果内存不能释放,这块内存就不能再次使用,我们就说这块内存泄漏了。

危害
内存泄漏的堆积,这会最终消耗尽系统所有的内存

image.png

举个例子:我们去饭店吃饭,正常的流程是开桌 -->吃饭-->结账走人-->服务员打扫桌子等待下一位客人使用。但是如果你吃完饭结账后一直赖着着不走,服务员就不会过来收拾桌子,那么这个桌子的内存就泄露了。

产生原因


罪魁祸首就是循环强引用。就像两个人打架一样,双方都让对象先放手,自己才回放手。



那在日常的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对象不能被释放的原因:


image.png

二者循环引用的列子

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对象不能被释放的原因:


image.png

正是类似这样的循环强引用,导致了内存不能释放! 那如何打破循环引用呢? 如果爱,请放手!

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的标志。

image.png

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. 被别人引用的对象才会阻碍该对象释放

image.png

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

举个例子


image.png

这个关注按钮的点击事件 我封装在一个工具内里面

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("图片下载成功!")
}

其他


当然除了闭包可能造成内存泄露外,还有其他的方式也可能造成。
例如:

  1. 不要让我们的对象被静态属性 或者 file层级的属性 所引用,这很容易造成内存泄漏。
  2. 注册类的工具,记得要注销,例如 通知 广播 Eventbus。
  3. 循环执行的计时器 或者线程,记得在合适的时候手动停止。

内存分析工具


无论是studio还是xcode都有自己的内存分析工具,当然也有第三方库来监听内存使用情况(自行百度哈),避免内存泄露还得从coding中做起,希望今天的分享能帮助到你。

相关文章

  • 移动端探讨闭包的内存泄露(公司分享)

    前言 随心所欲的闭包给我们的代码变了简洁很多,尤其是使用Kotlin和Swift做coding的时候,但是闭包虽好...

  • 闭包中关于内存

    在闭包中,回收使用的对象,避免内存泄露

  • 面试题 -- JS哪些操作会造成内容泄露

    意外的全局变量引起的内存泄露 闭包引起的内存泄露 闭包可以维持函数内局部变量,使其得不到释放。 上例定义事件回调时...

  • 闭包

    闭包就是内部的函数被保留到了外部 闭包会导致原有作用域链不释放,造成内存泄露。

  • javascript中的闭包

    闭包 基本概念 当 内部函数 被保存到 外部 时,将会一定生成闭包。闭包会导致原有作用域链不释放,造成内存泄露。...

  • 闭包

    闭包是一个非常强大的特性,但人们对其也有诸多误解。一种耸人听闻的说法是闭包会造成内存泄露,所以要尽量减少闭包的使用...

  • 哪些操作会造成内存泄?

    1)意外的全局变量引起的内存泄露 2)闭包引起的内存泄露 3)没有清理的DOM元素引用 4)被遗忘的定时器或者回调...

  • 杂谈:什么是闭包?闭包会造成内存泄露?

    为什么会流传闭包会导致内存泄露!因为IE浏览器早期的垃圾回收机制,有 bug。 IE浏览器中使用完闭包之后,依然回...

  • jQuery源码 数据缓存

    内存泄露的几种情况1.循环引用2.Javascript闭包3.DOM插入 为了避免内存泄漏,最好不要直接在DOM元...

  • 闭包

    概念 当内部函数被保存到外部时,将会生成闭包.闭包会导致原有的作用域链不释放造成内存泄漏.什么是泄露比如手中的撒子...

网友评论

      本文标题:移动端探讨闭包的内存泄露(公司分享)

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