app 内存优化笔记

阶段

1. Leaks 处理一
2. Leaks 处理二
3. 其他处理

简介

项目业务随着开发迭代趋于稳定的时候,进入一段空白期,但是对于我们程序猿来说闲暇不是件好事,要让自己忙碌起来,这个时候我们就可以考虑完善和优化我们的项目了,既能优化了我们的产品性能,又能提高了我们的专业技能。本次我这边简单写了个笔记,记录一下我们项目初步优化过程。

Leaks 处理一(MLeaksFinder)

我初期,选择了weread团队开发的MLeaksFinder,这款Lib有比较友好的提示页面,PS:不是所有的内存她都能检测到,目前只能自动地检测 UIViewController 和 UIView 相关的对象~!,比较方便的一点地方就是,这货Debug模式下,遇到leaks地方直接Alert view 告诉你,Release模式自动关闭,我用该库检测到了部分遗留未释放的VC和View,大部分都是NSTimerNSNotificationCenterBlockWebViewScriptMessageHandler ....引起的循环引用,;Lib git 传送门-->

就是个人页 Cell持有了NSTimer,在VC 消失后未对timer 进行invalidate操作;

Memory Leaks
(
    MineViewController,
    view
    tableView,
    minesettingCell
)

根据提示我们是不是很快发现问题并且定位到类,找到泄露位置修复它,是不是很简单~!

如果其他团队开发者选择无视这个提示,我们在基类里面设置断言,让他一次crash个够~

- (BOOL)willDealloc {
    __weak id weakSelf = self;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [weakSelf assertNotDealloc];
    });
    return YES;
}

- (void)assertNotDealloc {
    NSString *clasName = NSStringFromClass([self class]);
    if ([clasName isEqualToString:@"MineViewController"])
    {// 这里忽略不需要 断言的 vc
        return;
    }
    NSAssert(NO, @"断言 ---------------------> 泄漏了");
}

OK,这是比较简单便捷的方式,检测和处理循环引问题,我们可以借助这个Lib,但也不能完全指望它,论检测内存泄露,苹果自己提供的有专业工具Instruments,我们看~

Leaks 处理二 (Instruments)

Xcode的Instruments里面有一个Leaks工具,可以帮助你定位发生内存泄漏的代码段,以便修复问题。通过方式打开Instruments面板PS:也可在Xcode页面 Command + i 快捷键启动

打开 Instruments.png

选择Leaks工具,打开后界面如图:

打开 Leaks.png

选择Target,在右下角Display Setting面板的Call Tree,勾选Invert Call Tree和Hide System Libraries,方便接下来我们迅速查找有内存问题的代码段。

instruments-Leaks.png

上面"打钩"选项默认是不选的,我们通常根据自己的需求,把它们勾选上,可以帮你更快定位到关键的问题代码上。

  • Separate by Category:按类别做分析,这样可以清晰的看到吃资源的问题线程的分类。
  • Separate by Thread:按线程分开做分析,这样更容易揪出那些吃资源的问题线程。特别是对于主线程,它要处理和渲染所有的接口数据,一旦受到阻塞,程序必然卡顿或停止响应。
  • Invert Call Tree:反向输出调用树。把调用层级最深的方法显示在最上面,更容易找到最耗时的操作。
  • Hide System Libraries:隐藏系统库文件。过滤掉各种系统调用,只显示自己的代码调用。
  • Flattern Recursion:拼合递归。将同一递归函数产生的多条堆栈(因为递归函数会调用自己)合并为一条。

双击打开提示Leaks的函数,进入到可视代码页面,这里我们可以直观的看到内存泄露的地方,针对当前代码做出修改和优化:

instruments-Call [email protected]

既然找到了问题所在,我们修改着也比较容易了,毕竟代码都是人写的,看得懂就好改了

+(ShareManager *)shareManager
{
    static ShareManager *instance = nil;
    static dispatch_once_t oneToken;
    dispatch_once(&oneToken,^{
        instance = [[ShareManager alloc] init];
    });
    return instance;
}
- (instancetype)init
{
    if (self = [super init]) 
    {
        // 单例 设置通知监听,肯定是释放不了的; 所以这里报了警告⚠️
        // 改之前
        [[NSNotificationCenter defaultCenter] addObserverForName:UIApplicationDidFinishLaunchingNotification object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) {
            if([[NSUserDefaults standardUserDefaults]boolForKey:@"isIntoGuide"])
            {
                  [self setupXHLaunchAd];
            }
        }];
        //  改之后
        if([[NSUserDefaults standardUserDefaults]boolForKey:@"isIntoGuide"])
        {
             [self setupXHLaunchAd];
        }
    }
    return self;
}

其他处理

其实有很多时候,内存泄露对象占用内存并不是太多,比如我们看到的几~几百Bytes,我们发现他并解决掉就完了;

很多时候,在我们应用开发中图片和视频内存开销是相当大的,如果我们没有处理好渲染--加载--缓存--释放问题,应用内存分分钟飚到GB级别,并且分分钟就crash给你看;

因为时间有限和当前发现问题比较大的地方就是图片加载,所以这次主要对图片进行了优化,因为涉及到了高清大图片加载,在处理图片的时候,有很多方式,介绍三种种方式:

后端 + 前端

条件允许的情况下(可扩展性强)

  • 请后端帮忙搭建流媒体服务器,对图片进行压缩处理;
  • 前端定义不同的协议,根据参数不同去取 Small / Medium / Big 不同规格的图片;
  • 后端根据前端传递的不同参数,对图片进行压缩,生成新的image url给前端;
  • 前端再对不同参数请求下来的图片进行加载(也可以再次处理,只要运营同学不嫌图片不清楚);
    这样图片加载过程中,内存会降下来很多~!真的,亲试过~
WebP image

WebP,是一种同时提供了有损压缩与无损压缩的图片文件格式。现在主流应用界面需要大量图片来,可以嵌入 WebP 的解码包,能够节省用户流量,提升访问速度优势:对于 PNG 图片,WebP 比 PNG 小了45%。是jpg vs webp, 是不是很厉害~

jpg vs webp.png

想更多的了解WebP知识的童鞋点击 A new image format for the Web
libwebp下载地址 libwebp download

这里就介绍一下具体怎么用,我们通过pod 'SDWebImage/WebP' 引入经典图片加载库SDWebImageWebP 的支持模块;
使用方式上跟加载普通图片没什么区别,当然也需要让SDWebImage支持WebP,设置如下Build Settings ---> Preprocessor Macros 添加 SD_WEBP = 1

Build Settings.png

PS: 这里也需要后端的童鞋们协助,对图片统一转换成webp格式,这时候你再去测试,图片压缩了很多,既提高图片下载速度、也为用户节省了流量、同样也减轻图片加载时候内存占用~一举多得;

only 前端

如果两种方式都没有人提供支持的话,自己的活自己干,谁让你的应用内存占用太高呢,甚至老Crash呢~
自己加入一些机制来解决 预防 避免图片加载内存过大Crash问题,也在合适的时机去释放这些资源,不要常驻内存,使用户感觉用的越久应用越卡;

目前项目加载使用的是SDWebImage,相信在座的各位基本都用过,国内外太多的App使用其进行图片加载,很主流,但是在用SDWebImage加载多个图片过程中,加载几张图片就内存暴增严重导致崩溃。

SDWebImage.png
  • 图片压缩 / 缓存
    因为目前整个工程所有的图片加载都是用的此库,我们针对该库做一些优化,SDWebImage有一个SDWebImageDownloaderOperation类来执行下载操作的,我们抽丝剥茧的定位到了 UIImage+MultiFormat文件中的 sd_imageWithData:方法

+ (nullable UIImage *)sd_imageWithData:(nullable NSData *)data {
    if (!data) {
        return nil;
    }
    UIImage *image;
    SDImageFormat imageFormat = [NSData sd_imageFormatForImageData:data];
    if (imageFormat == SDImageFormatGIF) {
        image = [UIImage sd_animatedGIFWithData:data];
    }
#ifdef SD_WEBP
    else if (imageFormat == SDImageFormatWebP)
    {
        image = [UIImage sd_imageWithWebPData:data];
    }
#endif
    else {
      //⚠️⚠️⚠️发现这里面对图片的处理是直接按照原大小进行的,如果几千是分辨率这里导致占用了大量内存⚠️⚠️⚠️
        image = [[UIImage alloc] initWithData:data];  // ❗️ 这里这里这里~❗️
    
#if SD_UIKIT || SD_WATCH
        UIImageOrientation orientation = [self sd_imageOrientationFromImageData:data];
        if (orientation != UIImageOrientationUp) {
            image = [UIImage imageWithCGImage:image.CGImage
                                        scale:image.scale
                                  orientation:orientation];
        }
#endif
    }


    return image;
}

所以我们需要在这里对图片做一次等比的压缩,在UIImage+MultiFormat这个类里面添加如下压缩方法 compressImageWith:

+(UIImage *)compressImageWith:(UIImage *)image  
{  
    float imageWidth = image.size.width;  
    float imageHeight = image.size.height;  
    float width = 640;  
    float height = image.size.height/(image.size.width/width);  
    float widthScale = imageWidth /width;  
    float heightScale = imageHeight /height;  
    UIGraphicsBeginImageContext(CGSizeMake(width, height));  
      
    if (widthScale > heightScale) {  
        [image drawInRect:CGRectMake(0, 0, imageWidth /heightScale , height)];  
    }  
    else {  
        [image drawInRect:CGRectMake(0, 0, width , imageHeight /widthScale)];  
    }  
    UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();  
    UIGraphicsEndImageContext();  
    return newImage;  
}
      

然后我们在 红色❗️地方image = [[UIImage alloc] initWithData:data];
下面调用以下, 当data大于1M的时候做压缩处理:

if (data.length/1024 > 1024) {
    image = [self compressImageWith:image];
}

我们在SDWebImageDownloaderOperation 类中``URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:

  UIImage *image = [UIImage sd_imageWithData:self.imageData];
   //将等比压缩过的image在赋在转成data赋给self.imageData
  NSData *data = UIImageJPEGRepresentation(image, 1);
  self.imageData =  [NSMutableData dataWithData:data];
  • 图片释放
    本身我们图片缓存所有的库就是SDImageCache,我们在收到内存警告或者在合适的实际是可以执行下面两个方法清理图片缓存的;
- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];

      [[SDImageCache sharedImageCache]clearDisk];
      [[SDImageCache sharedImageCache]clearMemory];
}

- (void)dealloc{
      [[SDImageCache sharedImageCache]clearDisk];
      [[SDImageCache sharedImageCache]clearMemory];
}

时间有限,水平一般,也没别的手艺,这次先这么着了,下次有时间学学其他童鞋的优化方案~

你可能感兴趣的:(app 内存优化笔记)