iOS小结(五) 结合 Instrument 分析并解决memory issues

来源:互联网 发布:网络工作人员工资待遇 编辑:程序博客网 时间:2024/06/14 09:31


       memory通常遇到两种问题:

  •  memory 持续上涨
  •  memory leak
         memory leak我遇到过一个问题是因为几个小的变量因为new的没有delete。

        遇到这些问题,首先是要分析是什么原因导致 memory 上涨, 涨的又是什么?

        分析利器:Instrument

        对于profile, 之前知道CUDA有自己profile的可视化工具, 而 apple 的 Instrument 是分析 ios 和 mac 程序运行时间和 memory allocation 很有效的工具。

iOS没有自动垃圾回收

       iOS没有自动垃圾回收,只有Mac OS X有自动垃圾回收的选项,在build settings 里, Objective-C Garbage Collection。
       垃圾回收发生在程序运行时,当系统检测到内存不足时,开始清理。这是一个计算密集的过程,系统追踪所有的对象和引用,检测对象是否被正确引用,所以很容易引起程序中断。所以并不推荐使用自动垃圾回收。
       iOS没有垃圾回收,大家说的ARC是什么?是Automatic Preference Counting 的缩写, 自动引用计数。为了方便程序员,在Xcode4.2以后引入了ARC。
       当不使用ARC时,我们要手工管理内存计数:
       对象刚创建时,引用计数初始为1, 
       为保证对象存在,每次给对象创建引用,引用数加1,可以给对象发送retain消息:
[myFraction retain];
       不再需要该对象时,引用数减1,给对象发送release消息:
[myFraction release];
       当对象引用计数为0时,系统就知道不再需要使用了,可以释放其内存,通过给对象发送dealloc消息进行。如果这时有其他变量要释放,需要覆写改类的dealloc方法,否则就使用继承自NSObject的dealloc方法。

how to use instrument

        Instrument 有两种打开方式,一种是直接打开录制,另一种是在 xcode 里 debug 过程中跳转,或直接在 xcode 界面中看大概的 memory 和 cpu 占用。

        对于第一种,直接在Spotlight里搜Instrument,可以看到Instrument可以分析的东西有很多,其中我们最常用的就是 Allocation, Leak 和 time profile了。Allocation 是分析memory 最准的, Leak 可以看哪里有内存泄漏,而且能看到函数调用关系, time profile 就是看CPU 各进程运行时间了,可以看具体是哪个进程是瓶颈,进而优化程序运行速度。另外GPU Driver如果用了GPU,应该也是很有用的。


       下面以Allocation为例,解释如何分析memory issue。 好,点击Allocation 出现了下面的界面:


             可以用 Instrument 记录下来你的操作过程中内存的分配,从空间和时间两个维度,录制的结果也可以保存成.trace文件,也是用 Instrument 打开。 首先在红框1处选择要监控的硬件对应的程序,点击红框2处的红点开始录制, 这里录制的程序应该是已经下到手机上的,而不是正在debug。开始执行操作吧,哪里有问题就哪里再想办法再现该操作,记得用单一变量法来缩小范围。

             录制完毕,怎样分析呢,可以在上半部分的面板拖拽一个范围,也就是监控这段操作过程中,相对增长的内存空间。而有用的数据主要是看 Statistics 和 Call Trees,在上图中用红圈3标识,call trees 是这部分占用的内存的函数调用关系,可以帮助很有用的找到问题的来源,有向右箭头的地方可以展开,有时函数调用的很深,借助方向键很方便。

             我这里打开了一个我保存的一个allocation.trace, 通过statistics,就可以看到在这个过程中LoadModel没有释放,再看call Tree,两者结合就能定位。



            上面所说的第二种打开方式,是在debug中,程序已经下到手机上之后,点击下图左边这个pool一样的红圈圈出来的图标,就可以看到 CPU 和 memory 都有实时监控,只不过 debug模式可能不是很准,点击右侧界面的Profile in Instrument,就打开了Instrument,memory的监控是跳到 Leak 而不是 Allocation, 写好的程序应该时常有意识测一测,有没有leak, Leak这个入口也可以看allocation, 不过不是完全从程序一开始就监控,基准线低一点。


            leak 和 time profile用法类似,time profile测时间,可以比较主进程和其他进程主要是哪个进程里什么操作是瓶颈,进而优化程序。

how to fix

            分析到问题所在,一般来说要具体问题具体分析,有的时候是该释放的没有及时释放,更多的时候是想要释放的时候已经lose track了,拿不到那个指针。
            IBM开发者论坛上有一篇帖子写的很全面,可以学习一下。
           7 foolproof tips for iOS memory management using Swift and Objective-C

            像 autoreleasePool 和弱引用等都是很好的 tips。还有calloc 比 malloc 多了一些自动释放的场景等。
    有一个表很好指出了 new/delete 和 malloc/free 的异同点:
            
Feature                  | new/delete                     | malloc/free                   --------------------------+--------------------------------+------------------------------- Memory allocated from    | 'Free Store'                   | 'Heap'                         Returns                  | Fully typed pointer            | void*                          On failure               | Throws (never returns NULL)    | Returns NULL                   Required size            | Calculated by compiler         | Must be specified in bytes     Handling arrays          | Has an explicit version        | Requires manual calculations   Reallocating             | Not handled intuitively        | Simple (no copy constructor)   Call of reverse          | Implementation defined         | No                             Low memory cases         | Can add a new memory allocator | Not handled by user code       Overridable              | Yes                            | No                             Use of (con-)/destructor | Yes                            | No                            
            图像处理相关有些特殊的地方,
            比如下例用到 CGDataProviderRef 来从RGBA的图像流buffer获取 UIImage ,image拿到之后,不只CGImageRef要用CFRelease释放掉, CGDataProvider也要release掉,不然也是会导致lose reference的内存增长。
+ (UIImage*) getUIImageFromRGBAs : (ImageBuf*) imgBuf {    CGDataProviderRef providerRef = CGDataProviderCreateWithData(NULL, imgBuf.rawData, imgBuf.height * imgBuf.width * imgBuf.bytesPerPixel, NULL);        CGColorRenderingIntent renderingIntent = kCGRenderingIntentDefault;        CGImageRef imgRef = CGImageCreate(imgBuf.width, imgBuf.height, imgBuf.bitsPerComponent, imgBuf.bitsPerPixel, imgBuf.bytesPerRow, imgBuf.colorSpace, imgBuf.bitmapInfo, providerRef, NULL, NO, renderingIntent);        UIImage* newImg = [UIImage imageWithCGImage: imgRef];    CFRelease(imgRef);    CGDataProviderRelease(providerRef);    return newImg;}
            除了这些小的tips,下面我会重点讲我们遇到的一个问题和解决方案:用单例 keep 住一块内存,让所有的处理都公用这块内存。

static memory pool

            我们是一个图像处理的app,每次新选择一个照片或拍一张照片进行处理,满意的话save,share, 不满意的话再重新选照片。对于我们的 app 调试的时候遇到这样的问题:每次从拍照页面进到处理页面再回到拍照页面都会涨固定几兆的内存,(这里有一个小妙招,调试内存的时候为了放大问题,我们可以换大图调试,我用后置摄像头拍的照不压缩,占内存有32M左右,这样用Allocation分析,每次一个来回都涨32M,那就是有一张图没有释放。)用 Instrument 帮我们定位之后,原来是UI层和算法层的Image转buffer的部分calloc的内存没有release,麻烦的是内存是在中间层申请的,而这段内存在后续一直在使用,知道不再使用的时候已经在UI层了,这时并不能获取到申请内存的那个指针了,而他还和其他部分有reference,所以没有被垃圾回收。
            我们用keep住一段memory pool, 让每次转buffer时候都share这段内存。实现就是单例中static的思想。
#import "DataPool.h"@implementation DataPoolstatic Byte* oriImg;+ (Byte*) getDataRef  {          if( oriImg == nil) {              oriImg = (Byte*) calloc(DataPoolSize, sizeof(Byte));          }          return oriImg;}@end
            objective-c 里的 +initialize 刚好也是这个实现,static,只有在第一次使用该对象的时候才调用,下面这样写可以实现同样的功能。
+ (void) initialize {    oriImg = (Byte*) calloc(DataPoolSize, sizeof(Byte));}
            一种更高级的写法是像这样:
__attribute__((constructor))static void initialize_memPool() {    oriImg = (Byte*) calloc(DataPoolSize, sizeof(Byte));}__attribute__((destructor))static void destroy_memPool() {    free(oriImg);    oriImg = nil;}
          这样就在app启动之前调用init函数,在app结束的时候调用destroy函数。如果有main函数,这两个函数是会分别在main 函数调用之前和main函数结束之后调用。还可以设置不同的优先级

         上面都是在obj-c里,静态变量在swift里也是一样,只需要加上static关键字,就可以用类名调用到这个变量了,可以说全局可见。
class AppDelegate: UIResponder, UIApplicationDelegate {    var window: UIWindow?    static var referenceImage : [UIImage] = []    //....}
           这样在任何地方只需要用 AppDelegate.referenceImage 就可以 get 到这个变量。

讲在后面

           通过上面的种种方法,持续的memory increase降下来了,用了static memory pool, 整个app的memory的基准线被拉高了怎么办,这需要细节上再分析,不用的赶紧释放,不等ARC垃圾回收提前释放,另外memory收到warning的时候,app会收到一个消息出发下面这个函数,可以考虑在其kill掉一部分内存,注意正在占用的话要做哪些额外的处理。
    override func didReceiveMemoryWarning() {        super.didReceiveMemoryWarning()        // Dispose of any resources that can be recreated.    }


0 0
原创粉丝点击