iOS开发-编译-tableview优化

前言

晚上脑子比较糊, 很多梳理过的知识点都说不出来, 很尴尬.

重新写一下吧

编译流程

应用编译成包的流程:

  • 预编译: 对每一个文件的头文件展开, 宏定义的替换等操作
  • 编译:
    • 前端
      • 词法分析: 主要是生成一个一个的token
      • 语法分析: 生成抽象语法树, 简称AST, 生成中间代码IR
    • 中间代码:
      • 生成.ll后缀的文件, 作为后端的输入. 并且由优化器做相应等级优化
    • 后端:
      • .ll转化为汇编.s
      • .s文件转化为一个个.o的可执行文件
      • 生成一个macho的可执行文件
  • 链接:
    • exex()程序函数入口, 开启进程空间

    • 加载编译好的macho文件到内存

    • dyld链接器进行链接

    • dyld从共享缓存空间递归加载依赖库, 比如libsystem, libdispatch, libobjc, Foundation框架等系统动态库, 使用imagelist命令可以查看到按照顺序加载进的image镜像

    • rebase操作, 由于alsr的原因, 对machoDATA段的数据进行重定向

    • binding操作, 对于调用的外部符号, 进行绑定操作.

      • dyld_stub_binderobjc_msgSend符号进行绑定, 属于非懒加载符号.

      • 比如pirnt符号在Foundation库中, 在第一次调用到的时候, 会通过dyld_stub_binder进行一个地址的绑定操作获取到真实的符号地址进行加载.

    • Objc Setup操作, 注册类到全局表中, category加载, 保证sel唯一性修复验证等.

    • 加载load方法, C++构造函数, 静态变量初始化等.

    • 调用main函数, RunLoop跑起项目启动

补充

关于binding的补充:

根据符号的字符串找到对应的DATA段数据进行修改, 究竟是怎么操作的.下面是通过懒加载符号表找到符号字符串的过程, 对应的进行反推即可.

  • Lazy Symbol Pointers找到符号的相对位置, 比如是在当前表第一个. 下面以第一个位置为例子进行描述.
  • indirect Symbols找到第一个位置, 记录Data数据, 转化为十进制.
  • Symbols上面得到的十进制数就是该符号在这张表里的Index, 找到对应的数据, 获取到DATA数据.
  • String Table该表的基值+上面获取到的偏移值, 就可以找到对应的字符串.

TableView优化

主要参考 VVeboTableViewDemo

下面是几种优化方式里供参考:

  • 缓存行高和布局
  • 快速滑动优化
  • 延时加载图片
  • 异步绘制

关于异步绘制的, 基本上是重写了整个layer层, 自己项目里没有用到.

启动流程优化

启动分为pre-main阶段和main函数之后

main函数之前可以优化的点

从上面的启动分析可以得出

  • 减少的动态库加载可以优化启动时间
  • 减少类可以优化启动时间
  • 减少load方法的调用, 可以优化启动时间
  • 减少contrustor函数和减少静态变量的设置可以优化.

但是在实际测试看来, 上面这几种对启动时间的影响几乎是微乎其微.

所以这方面不需要进行过多的消耗, 二进制重排其实对启动时间优化也是有一定效果的.

个人实测的话, 对于我自身的项目来说, 并不是很明显.看一些大型应用, 大概有15%左右.

优化方案:

  • 利用脚本监测未调用的类的方法, 在项目中进行移除. 影响级别比较小.
  • 一些组件的load加载, 放在Initializers阶段执行.

main函数之主要就是业务堆积

didFinishLaunch内部

  • 首页显示前:
    • 基础配置
    • url的注册
    • 定位服务
    • 基础的SDK的初始化和设置
    • 资源库下载

首页内部

  • 首页构建:
    • 等待定位信息
    • 请求拿到数据
    • 首页请求
    • 渲染
  • 渲染完成

经过我的测试, 主要是耗时操作, 就是在资源库下载, 以及等待定位的过程中.

所以我做了以下几点优化:

  • 资源库的下载放在了异步线程, 设置了最大的5条线程做并发操作, 并且是在首页构建之后进行的.

  • 定位操作 主要是使用了缓存, 先拿到缓存数据, 因为一般医生的定位都是在同一个地方, 所以当我获取到新的定位信息的时候, 只需要判断缓存是否命中, 则可以进行相应的省略.

  • url的注册根据优先级和同步设置, 优先加载home的, 然后其他的异步加载.

  • 基础的SDK和初始化, 分了是否必须的, 如果是必须的就在didFinish内部进行进行初始化, 如果不是必须的就进行异步初始化.

    • 比如路由相关, 网络相关, 通知注册等必须在启动之前
    • 比如一些性能监控, 崩溃信息上传, 和首页无关的业务等, 可以放在启动之后做.

缓存行高和布局

这个相对比较好理解, 获取到网络数据大致流程如下:

- (void)configureNetDara {

    dispatch_async(dispatch_get_global_queue(0, 0), ^{

        NSDictionary *result = [NSDictionary dictionary];

        //第三方直接调
        JKModel *model = result;
        //计算处理Model, Layout, rowHeight
        NSMutableArray *layouts = [NSMutableArray array];
        //循环处理数据
        for (id item in model.items) {

            if ([item isKindOfClass:[JKItemModel class]]) {

                JKModelLayout *layout = [JKModelLayout new];

                layout.imgRect = CGRectMake(0, 0, 0, 0);

                layout.labelRect = CGRectMake(0, 0, 0, 0);

                layout.model = item;

                [layouts addObject:layout];

            }
            //不同的 model, 可以继续添加
        }        

        dispatch_async(dispatch_get_main_queue(), ^{

            //主线程拿到数据u,  直接扔给cell, layout
            // 大概就是这么一个节奏.

            JKModelLayout *layout = [JKModelLayout new];

            JKCell *cell = [JKCell new];

            cell.img.frame = layout.imgRect;

            cell.label.frame = layout.labelRect;

            cell.label.text = layout.model.labelText;
        });
    });
}

@interface JKModel : NSObject

@property (nonatomic, strong) NSMutableArray <NSDictionary *>*items;

@end

@interface JKItemModel : NSObject

@property (nonatomic, copy) NSString *labelText;

@property (nonatomic, copy) NSString *imgUrl;

@end

@interface JKModelLayout : NSObject

@property (nonatomic, assign) NSInteger rowHeight;

@property (nonatomic, assign) CGRect imgRect;

@property (nonatomic, assign) CGRect labelRect;

@property (nonatomic, assign) JKItemModel *model;

@end

@interface JKCell : UITableViewCell

@property (nonatomic, strong) UILabel *label;

@property (nonatomic, strong) UILabel *img;

@end

要注意异步队列里面对于内部数组操作的数据安全问题

快速滑动优化

如果tableView, 在非常快速滑动的时候, 前面积攒了大量的绘制任务, 可能造成性能的浪费. 所以有了如下方案:

  • 提前计算好, 滑动目标位置的indexPath忽略中间的渲染, 从而在快速滑动时候减少渲染任务.
- (void)scrollViewWillEndDragging:(UIScrollView *)scrollView withVelocity:(CGPoint)velocity targetContentOffset:(inout CGPoint *)targetContentOffset {
//1\. 停止拖拽后,预计滑动停止后到的偏移量
    NSIndexPath *ip = [self.tableView indexPathForRowAtPoint:CGPointMake(0, targetContentOffset->y)];
//2\. 当前区域可视的组内的渲染的IndexPath
    NSIndexPath *cip = [[self.tableView indexPathsForVisibleRows] firstObject];
//3\. 设置跳过的数量, 假设超过了这个数量, 则认为是快速滑动
    NSInteger skipCount = 8;
    if (labs(ip.row - cip.row) >= skipCount) {
    //3.1 目标位置获取到的需要显示的索引组
        NSArray *temp = [self.tableView indexPathsForRowsInRect:CGRectMake(0, targetContentOffset->y, self.tableView.width, self.tableView.height)];
        NSMutableArray *arrM = [NSMutableArray arrayWithArray:temp];
    //3.2 向上滑动, 加载更多
        if (velocity.y < 0) {
            NSIndexPath *indexPath = [temp lastObject];
            //停止后出现的索引仍然在数据源内
            if (indexPath.row + 3 < self.dataSource.count) {
                [arrM addObject:[NSIndexPath indexPathForRow:indexPath.row + 1 inSection:0]];
                [arrM addObject:[NSIndexPath indexPathForRow:indexPath.row + 2 inSection:0]];
                [arrM addObject:[NSIndexPath indexPathForRow:indexPath.row + 3 inSection:0]];
            } 
        }  else {
            //3.3 向下滑动,加载之前的数据

            NSIndexPath *indexPath = [temp firstObject];
            if (indexPath.row > 3) {
                [arrM addObject:[NSIndexPath indexPathForRow:indexPath.row - 3 inSection:0]];
                [arrM addObject:[NSIndexPath indexPathForRow:indexPath.row - 2 inSection:0]];
                [arrM addObject:[NSIndexPath indexPathForRow:indexPath.row - 1 inSection:0]];
            }
        }
        //加载全局的数组内
        [self.needLoadArray addObjectsFromArray:arrM];
    }
}

- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView {

    NSLog(@"滑动停止的时候");
    [self.needLoadArray removeAllObjects];

}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {

    JKCell *cell = [tableView dequeueReusableCellWithIdentifier:cellId];

    [self drawCell:cell withIndexPath:indexPath];
    cell.delegate = self;   // VC作为Cell视图的代理对象
    return cell;
}

- (void)drawCell:(JKCell *)cell withIndexPath:(NSIndexPath *)indexPath{

    JKModel *model = [self.dataSource objectAtIndex:indexPath.row];
    cell.selectionStyle = UITableViewCellSelectionStyleNone;
    [cell clear];
    cell.model = model;

    // 如果当前 cell 不在需要绘制的cell 中,则直接 pass
    if (self.needLoadArray.count > 0 && ![self.needLoadArray containsObject:indexPath]) {
        [cell clear];
        return;

    }

    if (_scrollToToping) {
        return;
    }
    [cell draw];
}

需要注意的点:

  • 布局要计算好, 在跳跃的过程中, 可以不加载图片文字等, 但是布局可以加载.
    • 如果在滑动过程中, 行高未设置好 ,那么在慢速滑动到中间跳过的布局的时候, 会出现那种跳跃的画面.
    • 显示画面不空白友好一点就是, 尽量加载简单的文字, 图片和复杂的文本放在draw方法里面去做.
  • 拖动结束的时候, 清空掉数据源, 可以保证页面的正常加载, 否则中间省略的加载页面数据就无法出来.

延迟加载图片

主要就是主线程注册一个Obsever的观察者, 监听kCFRunLoopDefaultMode下的kCFRunLoopBeforeWaiting状态的回调函数. 讲图片渲染的操作, 放在回调中处理.

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    NewsModel *model = [self.dataSource objectAtIndex:indexPath.row];
    DelayLoadImgCell *cell = [tableView dequeueReusableCellWithIdentifier:cellId];
    cell.selectionStyle = UITableViewCellSelectionStyleNone;
    cell.model = model;
    // 将加载绘制图片操作丢到任务中去
    [self addTask:^BOOL{
        [cell drawImg];
        return YES;
    }];
    return cell;
}

// 添加任务
- (void)addTask:(RunloopBlock)unit {
    [self.tasks addObject:unit];
    // 保证之前没有显示出来的任务,不再浪费时间加载

    if (self.tasks.count > self.max) {
        [self.tasks removeObjectAtIndex:0];
    }
}

//添加观察者 观察主线程
- (void)addRunloopObserver {
    CFRunLoopRef runloop = CFRunLoopGetCurrent();
    CFRunLoopObserverContext context = {
            0,
            (__bridge void *)(self),
            &CFRetain,
            &CFRelease,
            NULL
   };
   static CFRunLoopObserverRef defaultModeObsever;
   defaultModeObsever = CFRunLoopObserverCreate(NULL,                                             
                                                kCFRunLoopBeforeWaiting,
                                                YES,   // 是否重复观察                                                 NSIntegerMax - 999,
                                                &Callback, // 回掉方法,就是处于NSDefaultRunLoopMode时候要执行的方法
                                                &context);

   CFRunLoopAddObserver(runloop, defaultModeObsever, kCFRunLoopDefaultMode);
   CFRelease(defaultModeObsever);
}

static void Callback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {

    DelayLoadImgViewController *vc = (__bridge DelayLoadImgViewController *)(info);  // 这个info就是我们在context里面放的self参数

    if (vc.tasks.count == 0) {
        return;
    }
    BOOL result = NO;
    while (result == NO && vc.tasks.count) {

        // 取出任务
        RunloopBlock unit = vc.tasks.firstObject;
        // 执行任务
        result = unit();
        // 干掉已经执行的任务
        [vc.tasks removeObjectAtIndex:0];
    }
}

不是很常用, 但是也是很好的处理, 在做imgView.img = [UIImage imageWithName:@"图片"]; 设置的时候, 就是会有图片解码的操作, 所以可以先加载imgView对于image的赋值操作, 放入任务队列等待滑动模式退出切换回默认模式的时候进行图片的解码和渲染操作.

异步渲染

异步渲染是重写了整个layer层, 对优化有性能的极致要求的做了这件事. 我本身项目里是没有用到的, 主要就是文本, 图片基本上都被重写了.
YYkit
Graver
VVeboTableViewDemo

这些都是重写了整个layer层的操作, 侵入性强, 性能也强, 不过都不维护了现在, 我目前没用到, 没有过度优化的需求.

如果你正在跳槽或者正准备跳槽不妨动动小手,添加一下咱们的交流群1012951431来获取一份详细的大厂面试资料为你的跳槽多添一份保障。

总结

    1. 对于前面的缓存行高和布局是最常用的.
    1. 对于快速滑动优化可以和第一条结合使用, 效果不错.
    1. 对于特殊常见可以使用runloop机制进行不同时间段的渲染操作. 主要是把影响卡顿的操作放在不同的时机做, 本文只是一个例子
    1. 异步渲染跟着大牛走就好.
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容