首页 科技正文

ug环球手机版下载:iOS开发实践-OOM治理

admin 科技 2020-12-26 196 1

概览

提及iOS的OOM问题人人第一想到的应该更多的是内存泄露(Memory Leak),由于无论是从早期的MRC照样2011年Apple推出的ARC内存泄露问题一直是iOS开发者对照重视的问题,好比我们熟悉的 Instruments Leaks 剖析工具,Xcode 8 推出的 Memory Graph 等都是官方提供的内存泄露剖析工具,除此之外另有类似于FBRetainCycleDetector的第三方工具。不外事实上内存泄露仅仅是造成OOM问题的一个缘故原由而已,现实开发历程中造成OOM的缘故原由有许多,本文试图从实践的角度来剖析造成OOM的诸多情形以及解决设施。

造成OOM的缘故原由

造成OOM的直接缘故原由是iOS的 Jetsam 机制造成的,在Apple的 Low Memory Reports中注释了详细的运行情形:当内存不足时,系统向当前运行中的App提议applicationDidReceiveMemoryWarning(_ application: UIApplication) 挪用和 UIApplication.didReceiveMemoryWarningNotification 通知,若是内存仍然不够用则会杀掉一些后台历程,若是仍然吃紧就会杀掉当前App。

关于 Jetsam 实现机制实在苹果已经开源了XNU代码,可以在这里查看,焦点代码在 kern_memorystatus 感兴趣可以阅读,其中包罗了许多系统挪用函数,可以辅助开发者做一些OOM监控等。

一、内存泄露

内存泄露造成内存被持久占用无法释放,对OOM的影响小大由之,多数情形下并非泄露的类直接造成大内存占用而是无法释放的类引用了对照大的资源造成连锁反应最终形成OOM。一样平常剖析内存泄露的工具推荐使用Leaks,厥后Apple提供了对照利便的Memory Graph。

Instruments Leaks

Leaks应该是被所有开发者推荐的工具,险些搜索内存泄露就会提到这个工具,然则许多同伙不清晰实在当前Leaks的作用没有那么大,多数时刻内存泄露使用Leaks是剖析不出来的。不妨运行下面的一个再简朴不外的泄露情形(在一个导航控制器Push到下面的控制器然后Pop出去举行验证):

class Demo1ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.customView.block = {
            print(self.view.bounds)
        }
        self.view.addSubview(self.customView)
    }
    
    private lazy var customView:CustomView = {
        let temp = CustomView()
        
        return temp
    }()

    deinit {
        print("Demo1ViewController deinit")
    }
}


class CustomView:UIView {
    var block:(()->Void)?
}

上面这段代码有显著的循环引用造成的内存泄露,然则前面说的两大工具险些都无能为力,首先Leaks是:

*** 上有大量的文章去先容Leaks若何使用等以至于让有些同砚以为Leaks是一个无所不能的内存泄露剖析工具,事实上Leaks在当前iOS开发环境下检测出来的内存泄露对照有限。之以是这样需要先领会一个App的内存包罗哪几部门:

  1. Leaked memory: Memory unreferenced by your application that cannot be used again or freed (also detectable by using the Leaks instrument).

  2. Abandoned memory: Memory still referenced by your application that has no useful purpose.

  3. Cached memory: Memory still referenced by your application that might be used again for better performance.

Leaked memory正是Leaks工具所能发现的内存,这部门内存属于没有任何工具引用的内存,在内存流动图中是是不可达内存。

Abandoned memory在应用内存流动图中存在,然则由于应用程序逻辑问题而无法再次接见的内存。和内存泄露最主要的区别是它的引用(包罗强引用和弱引用)是存在的,然则不会再用了。好比上面的循环引用问题,VC被Pop后这部门内存首先照样在内存流动图中的,然则下次再push我们是建立一个新的VC而非使用原来的VC就造成上一次的VC成了废弃的内存。

若是是早期MRC下建立的工具遗忘release之类的使用Leaks是对照容易检测的,然则 ARC 下就对照少了,现实验证历程中发现更多的是引用的一些古老的OC库有可能泛起,纯Swift险些没有。

Abandoned memory事实上要比leak更难发现,关于若何使用Instruments辅助开发者举行废弃的内存剖析,参见官方Allocations工具的使用:Find abandoned memory

Memory Graph

固然Xcode 8 的Memory Graph也是一大利器,不外若是你这么想上面的问题很有可能会失望(如下图),事实上Memory Graph我明白有几个问题:第一是这个工具要想现实捕捉内存泄露需要多运行几回,往往一次运行历程是无法捕捉到内存泄露的;第二好比上面的子视图引起的内存泄露是无法使用它捕捉内存泄露信息的,VC pop之后它会以为VC没有释放它的子视图没有释放也是准确的,事实上VC就应该是被释放的,不外调整一下上面的代码好比删除self.view.addSubview(self.customView)后只管还存在循环引用然则却是可以检测到的(不外现实上怎么可能那么做呢),关于这个玄学问题没有找到相关的说明文档来注释。然则事实上 Memory graph 从来也没有声明自己是在解决内存泄露问题,而是内存流动图剖析工具,若是这么去想这个问题似乎也不算是什么bug。

第三方工具

事实上看到上面的情形信赖许多同砚会想要使用第三方工具来解决问题,好比人人用的对照多的MLeaksFinder和PLeakSniffer,两者不同之处是后者除了可以默认查出 UIViewController 和 UIView 内存泄露外还可以查出所有UIViewController属性的内存泄露算是对前者的一个弥补。固然前者还配合了 Facebook 的FBRetainCycleDetector可以剖析出循环引用泛起的引用关系辅助开发者快速修复循环引用问题。

不外惋惜的是这两款工具,甚至包罗 PLeakSniffer 的 Swift 版本都是不支持 Swift 的(准确的说是不支持Swift 4.2,缘故原由是Swift 4.2继续自 NSObject 的类不会默认添加 @objc 符号 class_copyPropertyList无法接见其属性列表,不仅如此Swift5.x中连添加 @objcMembers 也是没用的),然则 Swift 不是到了5.x才ABI稳固的吗?,再次查看 Facebook 的 FBRetainCycleDetector 自己就不不支持Swift,详细可以查看这个issue这是官方的回覆,若是稍微熟悉这个库原理的同砚应该也不难发现详细的缘故原由,从现在的情形来看当前 FBRetainCycleDetector 的原理在当前swift上是行不通的,究竟要获取工具结构以及属性在Swift 5.x上已经不可能,除非你将属性符号为@objc,这显然不现实,走 SWift 的Mirror当前又无法 setValue,以是研究了一下现在开源社区的情形险些没有类似OC的完善解决方案。

Deubgger的LeakMonitorService

LeakMonitorService是我们自己实现的一个Swift内存泄露剖析工具,主要是为领会决上面两个库当前运行在Swift 5.x下的问题,首先明确的是当前 Swift 版本是无法接见其非 @objc 属性的,这就无法监控所有属性,然则试想实在只要这个监控可以解决大部门问题它就是有价值的,而通常的内存泄露也就存在于 UIViewController 和 UIView 中,因此起点就是检测 UIViewController 和其根视图和子视图的内存泄露情形。

若是要检测内存泄露就要先知道是否被释放,若是是OC只要Swizzle dealloc方式即可,然则显然Swift中是无法Swizzle一个deinit方式的,由于这个方式自己就不是runtime method。最后我们确定的解决方案就是通过关联属性举行监控,详细的操作(详细实现后面开源出来):

  1. 使用一个聚集Objects纪录要监控存在内存泄露的工具
  2. 给NSObject添加一个关联属性:deinitDetector,类型为 Detector 作为NSObject的署理,Detector是一个class,内里引用一个block,在 deinit 时挪用这个 block 从Objects 中移除监控工具
  3. 在 UIViewController 初始化时给 deinitDetector 赋值举行监控,同时将自身添加到 Objects 数组代表可能会发生内存泄露,在 UIViewController 的将要释放时检测监控(一样平常稍微延迟一会)检测Objects是否存在当前工具若是是被准确释放由于其属性deinitDetector 会将其从 Objects 移除以是就不会有问题,若是泛起内存泄露deinitDetector的内部block不会挪用,此时当前控制器还在 Objects 中说明存在内存泄露
  4. 使用同样的方式监控UIViewController的根视图和子视图即可

需要说明的是监控UIViewController的时机,通常建议添加监控的时机放到viewDidAppear(),检测监控的时机放到viewDidDisappear()中。缘故原由是此时子视图相对来说已经完成结构(制止存在动态添加的视图没有被监控到),而检测监控的时机放到viewDidDisappear()中自然也不是所有挪用了viewDidDisappear()的控制器就一定释放了,可以在viewDidDisappear()中配合isMovingFromParentisBeingDi *** issed属性举行对照精准的判断。

常见的内存泄露

经由 LeakMonitorService 检测确实在产物中发现了少量的内存泄露情形,然则很有代表性,这里简朴的说一下,固然通俗的block循环引用、NSTimer、NotificationCenter.default.addObserver()等这里就不在先容了,产物检测中险些也没有发现。

1.block的双重引用问题

先来看一段代码:

class LeakDemo2ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let customView = CustomView()
        customView.block1 = {
            [weak self] () -> CustomSubView? in
            guard let weakSelf = self else { return nil }
            let customSubview = CustomSubView()
            customSubview.block2 = {
                 // 只管这个 self 已经是 weak 了然则这里也会泛起循环引用
                print(weakSelf)
            }
            return customSubview
        }
        
        self.view.addSubview(customView)
    }
    
    deinit {
        print("LeakDemo2ViewController deinit")
    }

}

private class CustomView:UIView {
    var block1:(()->CustomSubView?)?
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        if let subview = block1?() {
            self.addSubview(subview)
        }
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

private class CustomSubView:UIView {
    var block2:(()->Void)?
}

上面的代码逻辑并不庞大,customView 的 block 内部已经思量了循环引用将 self 声明为 weak 是没有问题的,出问题的是它的子视图又嵌套了一个 block2 从而造成了 block2 的嵌套引用关系,而第二个 block2 又引用了 weakSelf 从而造成循环引用(只管此时的self是第一个 block 内已经声明成 weakSelf)解决的设施很简朴只要内部的 block2 引用的 self 声明成weak就好了(此时形成的是[weak weakSelf]的关系)。那么为什么会这样的,内部 block2 接见的也不是当前VC的self工具,而是弱引用怎么会出问题呢?

缘故原由是当前控制器 self 首先强引用了customView,而customView又通过 addSubview() 强引用了customSubView,这样依赖实在 self 已经对 customSubView形成了强引用关系。然则 customSubview 自己引用的弱引用weakSelf吗?(注重是弱引用的weakSelf,不是weakSelf的弱引用),然则需要清晰一点就是外部的弱引用是block1对self的弱引用,也就是在weak table(Swift最新实现在Side table)内里会纪录block1的弱引用关系,然则block2是不会在这个表中的,以是这里照样一个强引用,最终造成循环引用关系。

Swift中的weakSelf和strongSelf

弥补一下OC中的weakSelf和strongSelf的内容,通常情形下常见的做法:

__weak __typeof__(self) weakSelf = self;
[self.block = ^{
    __strong __typeof(weakSelf)strongSelf = weakSelf;
    if (strongSelf) {
        strongSelf.title = @"xxx";
    }
}];

固然你可以用两个宏简化上面的操作:

@weakify(self);
[self.block = ^{
	 @strongify(self);
    if (strongSelf) {
        self = @"xxx";
    }
}];

上面 strongSelf 的主要目的是为了制止block中引用self的方式在执行历程中被释放掉造成逻辑无法执行完毕,swfit中怎么做呢,实在很简朴(method1和method2要么都执行,要么一个也不执行):

self.block = {
    [weak self] in
    if let strongSelf = self {
        strongSelf.method1()
        strongSelf.method2()
    }
}

然则下面的代码是不可以的(有可能会泛起method2不执行,然则method1会执行的情形):

self.block = {
    [weak self] in
    self?.method1()
    self?.method2()
}

2.delay操作

通常人人都很清晰 NStimer 会造成循环引用(只管在新的api已经提供了block形式,不必引用target了),然则很少注重 DispatchQueue.main.asyncAfter() 所实现的delay操作,而它的返回值是 DispatchWorkItem 类型通常可以用它来作废一个延迟操作,不外一旦工具引用了 DispatchWorkItem 而在block中又引用了当前工具就形成了循环引用关系,好比:

class LeakDemo3ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        
        self.delayItem = DispatchWorkItem {
            print("asyncAfter invoke...\(self)")
        }
        DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(3), execute: self.delayItem!)
    }
    
    deinit {
        print("LeakDemo3ViewController deinit")
    }
    
    private var delayItem:DispatchWorkItem?

}

3.内部函数

实在,若是是闭包人人平时写代码都市对照在意制止循环引用,然则若是是内部函数许多同砚就没有那么在意了,好比下面的代码:

class LeakDemo4ViewController: UIViewController {

    var block:(()->Void)?
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        func innerFunc() {
            print(self)
        }
        
        self.block = {
            [weak self] in
            guard let weakSelf = self else { return }
            innerFunc()
            print(weakSelf)
        }
    }
    
    deinit {
        print("LeakDemo4ViewController deinit")
    }

}

innerfunc() 中强引用了self,而 innerFunc 执行上下文是在block内举行的,以是理论上在block内直接接见了self,最终造成循环引用。内部函数在swift中是作为闭包来执行的,上面的代码等价于:

let innerFunc =  {
    print(self)
}

提及block的循环引用这里可以弥补一些情形不会造成循环引用或者是延迟释放的情形。特别是对于延迟的情形此次在产物中也做了优化,尽可能快速释放内存制止内存峰值过高。

a.首先pushViewController()和presentViewController()自己是不会引用当前控制器的,好比说下面代码不会循环引用:

let vc = CustomViewController()
vc.block = {
    print(self)
}
self.present(vc, animated: true) {
    print(self)
}

b.UIView.animation不会造成循环引用

UIView.animate(withDuration: 10.0) {
    self.view.backgroundColor = UIColor.yellow
}

c.UIAlertAction的handler不会引起循环引用(iOS 8 刚出来的时刻有问题)

let alertController = UIAlertController(title: "title", message: "message", preferredStyle: UIAlertController.Style.alert)
let action1 = UIAlertAction(title: "OK", style: UIAlertAction.Style.default) { (alertAction) in
    print(self)
}
let action2 = UIAlertAction(title: "Cancel", style: UIAlertAction.Style.cancel) { (alertAction) in
    print(self)
}
alertController.addAction(action1)
alertController.addAction(action2)
self.present(alertController, animated: true) {
    print(self)
}

d.DispatchQueue asyncAfter会让引用延迟,这里的引用也是强引用,然则当asynAfter执行竣事会获得释放,然则不实时

DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(10)) {
    print(self)
}

e. *** 请求会延迟释放

如下在请求回来之前self无法释放:

guard let url = URL(string:"http://slowwly.robertomurray.co.uk/delay/3000/url/http://www.google.co.uk
") else { return }
let dataTask = URLSession.shared.dataTask(with: url) { (data, response, error) in
    print(self,data)
}
dataTask.resume()

f.其他单例工具有可能延迟释放,由于单例自己对外部工具强引用,只管外部工具不会强引用单例,不外释放是延迟的

class SingletonManager {
    static let shared = SingletonManager()
    
    func invoke(_ block: @escaping (()->Void)) {
        DispatchQueue.global().async {
            sleep(10)
            block()
        }
    }
}

SingletonManager.shared.invoke {
    print(self)
}

Instruments Allocation

前面说过Leaks和Memory Graph的限制,使用监控UIViewController或者UIView的工具对多数内存举行监控,然则究竟这是多数情形,有些情形下是无法监控到的,那么此时配合Instruments Allocation就是一个对照好的选择,首先它可以通过快照的方式快速查对比内存的增进点也就可以辅助剖析内存不释放的缘故原由,另外可以通过它查看当前内存被谁占用也就有利于辅助我们剖析内存占用有针对性行的举行优化。

首先要领会,当我们向操作系统申请内存时系统分配的内存并不是物理内存地址而是虚拟内存 VM Regions 的地址。每个历程拥有的虚拟内存的空间巨细是一样的,32位的历程可以拥有4GB的虚拟内存,64位历程则更多。认真正使用内存时,操作系统才会将虚拟内存映射到物理内存。以是理论受骗两个历程A和B默认拥有相同的虚拟内存巨细,当B使用内存时发现物理内存已经不够用在OSX上会将不活跃内存写入硬盘,叫做 swapping out。然则在iOS上面会直接发出内存忠告 Memory warning 通知App清算无用内存(事实上也会引入 Compressed memory 压缩一部门内存,需要的时刻解压)。

固然要使用这个工具之前建议先领会这个工具对内存种别划分:

  • All Heap Allocations :历程运行历程中堆上分配的内存,简朴明白就是现实分配的内存,包罗所有的类实例,好比UIViewController、UIView、Foundation数据结构等。好比:
    • Malloc 512.00KiB: 分配的512k堆内存,类似另有 Malloc 80.00KiB
    • CTRun: Core Text工具内存
  • All Anonymous VM :主要包罗一些系统模块的内存占用,以 VM: 开头
    • VM:CG raster data:(光栅化数据,也就是像素数据。注重纷歧定是图片,一块显示缓存里也可能是文字或者其他内容。通常每像素消耗 4 个字节)
    • VM:Statck:栈内存(好比每个线程都市需要500KB)
    • VM:Image IO:(图片编解码缓存)
    • VM:IOSurface:用于存储FBO、RBO等渲染数据的底层数据结构,是跨历程的,通常在CoreGraphics、OpenGLES、Metal之间通报纹理数据。
    • CoreAnimation: 动画资源占用内存
    • VM:IOAccelerator:图片的CVPixelBuffer

需要注重,Allocations统计的 Heap Allocations & Anonymous VM(包罗:All Heap AllocationsAll Anonymous VM) 并不包罗非动态的内存,以及部门其他动态库建立的VM Region(好比:WebKit,ImageIO,CoreAnimation等虚拟内存区域),相对来说是低于现实运行内存的。

为了进一步领会内存现实分配情形,这里不妨借助一下 Instruments VM Tracker 这个工具,对于前面说过虚拟内存,这个工具是可以对虚拟内存现实分配情形有直观展示的。

Virtual memory(虚拟内存) = Dirty Memory(已经写入数据的内存) + Clean Memory(可以写入数据的清洁的内存) + Compressed Memory(对应OSX上的swapped memory)

Dirty Memory : 包罗所有 Heap 中的工具、以上All Anonymous VM以及每个framework的 _DATA 段和 _Dirty_Data 段

Clean Memory:可以写数据的清洁的内存,不外对于开发者是read-only,操作系统卖力写入和移除,好比:System Framework、Binary Executable占用的内存,framework都有_DATA_CONST段(不外当使用framework时会酿成 Dirty memory )

Compressed Memory:由于iOS系统是没有 swapped memory 的,取而代之的是 Compressed Memory ,通过压缩内存可以降低也许一半的内存。不外遇到内存忠告释放内存的时刻情形就庞大了些,好比遇到内存忠告后通常可以试图压缩内存,而这时开发者会在收到忠告后释放一部门内存,遇到释放内存的时刻内存很可能会从压缩内存再解压去释放反而峰值会增添。

前面提到过 Jetsam 对于内存的控制机制,这里需要明确它做出内存忠告的依据是 phys_footprint,而发生内存忠告后系统默认清算的内存是 Clean Memory 而不会清算 Dirty Memory,究竟有数据的内存系统也不知道是否另有用,无法自动清算。

Resident Memory = Dirty Memory + Clean Memory that loaded in physical memory

Resident Memory:已经被映射到虚拟内存中的物理内存,然则注重只有 phys_footprint 才是真正消耗的物理内存,也正是 Jetsam 判断内存忠告的依据。

Memory Footprint:App 现实消耗的物理内存,Jetsam 判断内存忠告的依据,包罗:Dirty Memory 、Compressed Memory、NSCache, Purgeable、IOKit used
和部门加载到物理内存的Clean memory。

若是简朴总结:
Instruments AllocationsHeap Allocations & Anonymous VM 是整个App占用的一部门,它又分为 Heap Allocations 为开发者申请的内存,而 Anonymous VM 是系统分配内存(然则并不是不需要优化)。这部门只管不是 App 的所有消耗内存但却是开发者最关注的。

Instruments VM TrackerDirty MemorySwapped(对应iOS中的 Compressed Memory) 应该是开发者关注的主要内存占用,对照接近于现实占用内存,类似的是Xcode Navigator的内存也接近于最终的 Memory Footprint (多了调试占用的内存而已一样平常可以以为是 App 现实占用内存)

关于图片的内存占用有需要注释一下:CGImage 持有原始压缩花样DataBuffer(DataBuffer占用自己对照小),通过类似引用计数治理真正的Image Bitmap Buffer,需要渲染时通过 RetainBytePtr 拿到 Bitmap Buffer 塞给VRAM(IOSurface),不渲染时 ReleaseBytePtr 释 放Bitmap Buffer。通常在使用UIImageView时,系统会自动处明白码历程,在主线程上解码和渲染,会占用CPU,容易引起卡顿。推荐使用ImageIO在后台线程执行图片的解码操作(可参考SDWebImageCoder)。然则ImageIO不支持webp。

二、持久化工具

许多时刻内存泄露确实可以很大水平上解决OOM问题,由于类似于UIViewController或者UIView中包罗大量UIImageView的情形下,两者不释放很可能会有很大一块关联的内存得不到释放造成内存泄露。然则另一个问题是持久化工具,纵然解决了所有内存泄露的情形也并不代表就真正解决了内存泄露问题,其中一个主要的因素就是持久化工具。

关于持久化工具这里主要指的是类似于App进入后在主界面永远不会释放的工具,以及某些单例工具。象基本上基本上不kill整个app是无法释放的,然则若是由于设计缘故原由又在首页有大量这样的持久工具那么OOM的问题理论上加倍难以解决,由于此时要修改整个App结构险些是不可能的。

这里简朴对非泄露OOM情形举行分类:

  1. 首页及其关联页面:好比首页是UITabbarController响应的tab点击之后也成为了持久化工具无法释放
  2. 单例工具:特别是会加载一些大模子的单例,好比说单例中封装了人脸检测,若是人脸检测模子对照大,首次使用人脸识别时加载的模子也会永远得不到释放
  3. 庞大的界面层级:Push、Pop是iOS常用的导航操作,然则若是界面设计过于庞大(甚至可以无限Push)那么层级深了以后前面UINavigationController栈中的工具一直堆叠也会OOM
  4. 耗资源的工具:好比说播放器这种消耗资源的工具,理论上不会在同一个app内播放两个音视频,设计成单例反而是对照好的方案
  5. 图片资源:图片资源是app内最占用内存的资源,一个不合适的图片尺寸就可以导致OOM,好比一张边长10000px的正方形图片解码后的巨细是10000 * 10000 * 4 = 381M左右

首先说一下第一种情形,实在在早期iOS中(5.0及其之前的版本)针对以上情形有内存警lunload机制,通常在viewDidUnload()中释放当前view,同时也是给开发者提供资源卸载的一个对照合适的时机,当UIViewController再次展示时会重新loadView(),而从iOS 6.0之后Apple建议相关操作放到didReceiveMemoryWarning()方式中,主要的缘故原由是由于仅仅释放当前根视图并不会带来大的内存释放同时又造成了体验问题,原本一个UITableView已经翻了几页了现在又要重新加载一遍。以是结论是在didReceiveMemoryWarning()放一些大的工具释放操作,而不建议直接释放view,然则不管怎么样一定要做恢复机制。现实的实践是在我们的MV播放器中做了卸载操作,由于MV的预览要经由A->B->C的push历程,A、B均包罗了MV预览播放器,而现实测试两个播放器的内存占用也许110M上下这是一部门很大的开销,特别是对于iPhone 6等1g内存的手机。另外针对某个页面有多个子控制器的情形制止一次加载所有的自控制器的情形,理想的情形是切换到对应的控制器时才会加载对应的控制器。

单例工具是另一种大内存持久工具,通常情形下工具自己占用内存很有限,做成单例没有什么问题,然则这个工具引用的资源才是关注的重点,好比说我们产物中中有个主体识别模块,依赖于一个AI模子,自己这个模块也并非App操作的必经路径,首次使用时加载,然则之后就不会释放了,这样一来对于使用过一次的用户很有可能不再使用就没需要一直占用,解决的设施自然是不用单例。

关于庞大的界面层级则完全是设计上的问题,只能通过界面交互设计举行控制,而对于耗资源工具上面也提到了只管复用同一个工具即可,这里不再赘述。

此外,前面说到FBO相关的内存,实在这部门内存也是需要手动释放的,好比在产物中使用的播放器在用完之后并没有实时释放,挪用 CVOpenGLESTextureCacheFlush() 实时清算(类似的另有使用基于OpenGL的滤镜)。

内存峰值飙升

除了持久的内存占用意外,有时会不适当的操作会造成内存的飙升泛起OOM,只管这部门内存可能一会会被释放掉不会恒久的占用内存然则内存的峰值自己就是很危险的操作。

图片压缩

首先重点关注一下图片的内存占用,图片应该是最占用内存的工具资源,理论上UILayer最终展示也会绘制一个bitmap,不外这里主要说的是UIImage资源。一张图片要最终展示出来要经由解码、渲染的步骤,解码操作的历程就是就是从data到bitmap的历程,这个历程中会占用大量内存,由于data是压缩工具,而解码出来的是实实在在的像素信息。自然在开发中重用一些控件、做图片资源优化是需要的,不外这些事实上在我们的产物中都是现成的内容,若何进一步优化是我们最关注的的。理论上这个问题可以归结到第一种情形的范围,就是若何让首页的图片资源尽可能的小,谜底也是显而易见的:第一解码历程中尽可能控制峰值,第二能用小图片的绝不解码一张大图片。

好比一个图片压缩需求一张伟大的图片要判断图片巨细做压缩处置,假设这张图片是1280 * 30000的长图,原本的目的是要判断图片巨细举行适当的压缩,好比说跨越50M就举行80%压缩,若是100M就举行50%压缩,然则遇到的情形是这样的:原本为了判断图片的巨细以及保留新的图片,原图片A内存占用约莫146M,声明晰一个新工具B保留压缩后的图片,然则默认值是A原图,凭据情形给B赋值,现实情形是原图146M+146M+中心压缩效果30M左右,当前内存322M直接溃逃。优化这个操作的历程自然是只管少建立中心变量,也不要赋值默认值,制止峰值溃逃。

关于产物中使用合适的图片应该是多数app都市遇到的情形,好比首页默认有10张图,原本尺寸是对照小的UIImageView也没有需要使用过大的图片,不外现实情形很可能是通过后端请求的url来加载图片。好比说一个64pt * 64pt的UIImageView要展示一个1080 * 1920 pixal的图片内存占用达在2x情形下多了126倍之多是完全没需要的,不外后端的设置自然是不可信的,纵然刚开始没有问题说不准后面运营维护的时刻上一张超大的图片也是很有可能的。解决方式自然是向下采样,不外这里建议不要直接使用Core Graphics绘制,制止内存峰值过高,Apple也给了推荐的做法。

常见的压缩方式:

func compressImage(_ image:UIImage, size:CGSize) -> UIImage? {
        let targetSize = CGSize(width: size.width*UIScreen.main.scale, height: size.height*UIScreen.main.scale)
        UIGraphicsBeginImageContext(targetSize)
        image.draw(in: CGRect(origin: CGPoint.zero, size: targetSize))
        let newImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
        return newImage
    }

推荐的做法:

func downsamplingImage(url:URL, size:CGSize) -> UIImage? {
        let imageSourceOptions = [kCGImageSourceShouldCache:false] as CFDictionary
        guard let imageSource = CGImageSourceCreateWithURL(url as CFURL, imageSourceOptions) else { return nil }
        let maxDimension = max(size.width, size.height) * UIScreen.main.scale
        let downsamplingOptions = [
            kCGImageSourceCreateThumbnailFromImageAlways : true,
            kCGImageSourceShouldCacheImmediately : true ,
            kCGImageSourceCreateThumbnailWithTransform:true,
            kCGImageSourceThumbnailMaxPixelSize : maxDimension
        ] as CFDictionary
        guard let downsampleImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, downsamplingOptions) else { return nil }
        let newImage = UIImage(cgImage: downsampleImage)
        return newImage
    }

大量循环操作

此外关于一些循环操作,若是操作自己对照耗内存,通常的做法就是使用 autoreleasepool 确保一个操作完成后内存实时释放,然则在PHImageManager获取图片时这种方式并不是太凑效。好比说下面的一段代码获取相册中30张照片保存到沙盒:

guard let cachePath = NSSearchPathForDirectoriesInDomains(.cachesDirectory, FileManager.SearchPathDomainMask.userDomainMask, true).first else { return }
let assets = getAssets() // top 30
for i in 0..<assets.count {
    let option = PHImageRequestOptions()
    option.isSynchronous = false
    option.isNetworkAccessAllowed = true
    PHImageManager.default().requestImage(for: assets[i], targetSize: CGSize(width: 1080, height: 1920), contentMode: PHImageContentMode.aspectFit, options: option) { (image, info) in
        if info?[PHImageResultIsDegradedKey] as? Bool == true {
            return
        }
        if let image = image {
            do {
                let savePath = cachePath + "/\(i).png"
                if FileManager.default.fileExists(atPath: savePath) {
                    try FileManager.default.removeItem(atPath: savePath)
                }
                try image.pngData()?.write(to: URL(fileURLWithPath: savePath))
            } catch {
                print("Error:\(error.localizedDescription)")
            }
        }
    }
}

实测在iOS 13下面内存峰值85M左右,执行后内存65M,比执行前多了52M而且这个内存应该是会一直常驻,这也是网上许多文章中提到的增添autoreleasepool来实时释放内存的缘故原由。革新之后代码:

guard let cachePath = NSSearchPathForDirectoriesInDomains(.cachesDirectory, FileManager.SearchPathDomainMask.userDomainMask, true).first else { return }
let assets = getAssets()
for i in 0..<assets.count {
    autoreleasepool(invoking: {
        let option = PHImageRequestOptions()
        option.isSynchronous = false
        option.isNetworkAccessAllowed = true
        PHImageManager.default().requestImage(for: assets[i], targetSize: CGSize(width: 1080, height: 1920), contentMode: PHImageContentMode.aspectFit, options: option) { (image, info) in
            if info?[PHImageResultIsDegradedKey] as? Bool == true {
                return
            }
            if let image = image {
                do {
                    let savePath = cachePath + "/\(i).png"
                    if FileManager.default.fileExists(atPath: savePath) {
                        try FileManager.default.removeItem(atPath: savePath)
                    }
                    try image.pngData()?.write(to: URL(fileURLWithPath: savePath))
                } catch {
                    print("Error:\(error.localizedDescription)")
                }
            }
        }
    })
}

实测之后发现内存峰值降低到了65M左右,执行之后内存在50M左右,也就是峰值和之后常驻内存都有所降低,autoreleasepool有一定作用,然则作用不大,然则理论上这个常驻内存应该恢复到之前的10M左右的水平才对为什么多了那么多呢?缘故原由是Photos获取照片是有缓存的(注重在iPhone 6及以下装备不会缓存),这部门缓存若是进入后台会释放(主要是IOSurface)。实在这个历程中内存主要包罗两部门 IOSurface 和 CG raster data ,那么想要降低这两部门内存实在针对上述场景更好的设施是使用 PHImageManager.default().requestImageDataAndOrientation() 而不是 PHImageManager.default().requestImage() 实测上述情形内存峰值 18M 左右而且瞬间可降下来。那么若是需求场景非要使用 PHImageManager.default().requestImage() 怎么办呢?谜底是使用串行操作降低峰值。

guard let cachePath = NSSearchPathForDirectoriesInDomains(.cachesDirectory, FileManager.SearchPathDomainMask.userDomainMask, true).first else { return }
let semaphore = DispatchSemaphore(value: 0)
self.semaphore = semaphore
DispatchQueue.global().async {
    let assets = self.getAssets()
    for i in 0..<assets.count {
        print(1)
        autoreleasepool(invoking: {
            let option = PHImageRequestOptions()
            option.isSynchronous = false
            option.isNetworkAccessAllowed = true
            PHImageManager.default().requestImageDataAndOrientation(for: assets[i], options: option) { (data, _, orientation, info) in
                if info?[PHImageResultIsDegradedKey] as? Bool == true {
                    return
                }
                defer {
                    semaphore.signal()
                    print(4)
                }
                do {
                    print(3)
                    let savePath = cachePath + "/\(i).png"
                    if FileManager.default.fileExists(atPath: savePath) {
                        try FileManager.default.removeItem(atPath: savePath)
                    }
                    try data?.write(to: URL(fileURLWithPath: savePath))
                } catch {
                    print("Error:\(error.localizedDescription)")
                }
            }
        })
        print(2)
        _ = semaphore.wait(timeout: .now() + .seconds(10))
        print(5)
        
    }
}

通过串行控制以后内存峰值稳固在16M左右,而且执行之后内存没有显著增进,然则响应的操作效率自然是下降了,整体时长增高。

总结

本文从内存泄露和内存占用两个角度剖析领会决OOM的问题,也是产物中现实遇到问题的一次彻查效果,列举了常见引起OOM的缘故原由,也对持久内存占用给了一些实践的建议,对于对照难发现的leak情形做了示例演示,也是产物现实遇到的,事实上在我们的产物中通过上面的手段OOM降低了80%以上,整体的App框架也并没有做其他修改,以是有类似问题的同砚不妨试一下。

,

apple developer enterprise account for rent

providing apple enterprise developer accounts for rent, rent your own enterprise account for app signing. with high quality, stable performance and affordable price.

版权声明

本文仅代表作者观点,
不代表本站Allbet欧博官网的立场。
本文系作者授权发表,未经许可,不得转载。

评论

精彩评论
  • 2020-12-26 00:01:03

    电银付安装教程(dianyinzhifu.com)是官方网上推广平台。在线自动销售电银付激活码、电银付POS机。提供电银付安装教程、电银付使用教程、电银付APP使用教程、电银付APP安装教程、电银付APP下载等技术支持。面对全国推广电银付加盟、电银付大盟主、电银付小盟主业务。睡前标配