几道iOS面试题(四)

479 阅读7分钟
  1. 讲一讲UITableview的优化方法
  2. 有没有用过运行时,用它都能做什么?
  3. SDWebImage的缓存策略?
  4. AFN为什么添加一条常驻线程?
  5. KVO的使用?实现原理?
  6. KVC的使用?实现原理?

讲一讲UITableview的优化方法

  • 缓存高度

    提前计算好 cell 的高度和布局,做好缓存。可以尝试用key-value方式存储

    // 关于UITableView有两个重要的方法
    - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath;
    - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath;
    
  • 异步绘制

    在Cell上添加系统控件的时候,实质上系统都需要调用底层的接口进行绘制,当我们大量添加控件时,对资源的开销也会很大,所以我们索性直接绘制,提高效率。

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        CGRect rect = CGRectMake(0, 0, 100, 100);
        UIGraphicsBeginImageContextWithOptions(rect.size, YES, 0);
        CGContextRef context = UIGraphicsGetCurrentContext();
        [[UIColor lightGrayColor] set];
        CGContextFillRect(context, rect);
     
        //将绘制的内容以图片的形式返回,并调主线程显示
        UIImage *temp = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
     
        dispatch_async(dispatch_get_main_queue(), ^{
            // 回到主线程
        });
    });
    
  • 减少层级

    减少SubViews的数量, 在滑动的列表上,多层次的view会导致帧数的下降。 例如: 绘制cell 不建议使用UIView,建议使用CALayer(Core Animation)

  • Hide(显示隐藏)

    尽量少用addView给Cell动态添加View,可以初始化时就添加,然后通过hide来控制是否显示

  • 避免离屏渲染

    • 离屏渲染。

      在GPU在当前屏幕缓冲区以外开辟一个缓冲区进行渲染操作。俗称 GPU根据画家算法没办法通过一次绘制成功的。

      以下功能会触发离屏渲染:

      • 允许边界剪切 masksToBounds 、clipsToBounds = YES
      • 允许不透明 layer.allowsGroupOpacity =YES
      • 不透明度 layer.opacity < 1.0
      • 组不透明度 group opacity
      • 阴影 layer.shadow
      • 遮罩 layer.mask
      • 模糊效果 UIBlurEffect,同样无法通过一次遍历完成
      • layer.edgeAntialiasingMask 边缘抗锯齿,layer.allowsEdgeAntialiasing的图层文本(任何种类,包括UILabel,CATextLayer,Core Text等)
      • 使用CGContext在drawRect :方法中绘制大部分情况下会导致离屏渲染,甚至仅仅是一个空的实现。

      监测工具:

      ​ Xcode->Open Develeper Tools->Instruments中的 Core Animation

      优化:

      • 使用CAShapeLayer ,它属于CoreAnimation
      • 封装贝塞尔曲线去绘制
      • 阴影shadow 用 shadowPath
      • 对于view的圆形边框,如果没有backgroundColor,可以放心使用cornerRadius来做
      • 对layer进行异步渲染(Facebook开源的异步绘制框架AsyncDisplayKit)
      • 设置layer的opaque不透明值=YES,减少复杂图层合成
      • 尽量使用不包含透明alpha通道的图片资源
      • 尽量设置layer的大小值为整形值
      • 对于模糊效果,不采用系统提供的UIVisualEffect,而是另外实现模糊效果CIGaussianBlur,并手动管理渲染结果

      延伸:

      • CALayer 为离屏渲染 提供了对应的解法:shouldRasterize

        一旦被设置为true,Render Server就会强制把layer的渲染结果(包括其子layer,以及圆角、阴影、group opacity等等)保存在一块内存中,这样一来在下一帧仍然可以被复用,而不会再次触发离屏渲染。

  • 其他的优化方法:

    • 正确地使用UITableViewCell,和collectionView的dequeueReusableCellWithReuseIdentifier重用机制
    • 避免阻塞主线程,尽量开辟子线程操作耗时功能
    • 懒加载
    • 尽可能重用开销比较大的对象,比如复用缓存
    • 尽量减少计算的复杂度,比如开辟@autorelease来计算区间算法做好回收

有没有用过运行时,用它都能做什么?

  • 通过addMethod添加方法

  • 通过关联给category 模拟添加属性

  • 通过 changeMethod 交换方法

  • 访问私有属性

  • 改变isa指针等等等

    场景:button防止暴力点击、数组防nil越界、view绑定方法等等等等


SDWebImage的缓存策略?

SDWebImage 的图片缓存采用的是 MemoryDisk双重缓存机制。

  1. 图片读取

    调用 sd_setImageWithURL

  2. 查询缓存

    SDImageCache 类的 queryDiskCacheForKey 方法内部先查询 Memory Cache(内存缓存),查不到再查询 Disk Cache(内存缓存)

    当然硬盘缓存里查到了还是会存入内存缓存里。

  3. 请求网络

    请求网络使用的是 imageDownloader属性,这个示例专门负责下载图片数据。

    如果下载失败, 会把失败的图片地址写入 failedURLs 集合

    if ( error.code != NSURLErrorNotConnectedToInternet
      && error.code != NSURLErrorCancelled
      && error.code != NSURLErrorInternationalRoamingOff
      && error.code != NSURLErrorDataNotAllowed
      && error.code != NSURLErrorCannotFindHost
      && error.code != NSURLErrorCannotConnectToHost) {
    		@synchronized (self.failedURLs) {
    			[self.failedURLs addObject:url];
    		} // 为什么要有这个 failedURLs 呢
    }// 因为 SDWebImage 默认会有一个对上次加载失败的图片拒绝再次加载的机制。一张图片在本次会话加载失败了,如果再次加载就会直接拒绝。
    // 请求的时候设置 SDWebImageRetryFailed 标记可以解决这个加载失败导致不会再次加载的问题
    
  4. 写入缓存

    使用 [self.imageCache storeImage] 方法将它写入缓存,并且调用 completedBlock 告诉前端显示图片。

  5. 缓存清理

    在每次 APP结束的时候执行清理任务。

    • 第一步 先清除掉过期的缓存文件。 如果清除掉过期的缓存之后,空间还不够。

    • 第二步 继续按文件时间从早到晚排序,先清除最早的缓存文件,直到剩余空间达到要求。

      maxCacheAge 属性是文件缓存的时长。默认值 = 7天

      maxCacheSize 控制 SDImageCache 所允许的最大缓存空间,单位字节。 没有默认值。


AFN为什么添加一条常驻线程?

AFN3.x 已经解决这个问题。替代方案就是NSURLSession

在2.x版本的AFN中,使用的是NSURLCollection进行封装。NSURLConnection的网络请求是异步发起的,事件和结果的回调在原来线程的RunLoop中进行。

  • 在请求完成后我们需要对数据进行一些序列化处理,或者错误处理。如果我们在主线中处理这些事情很明显是不合理的。不仅会导致UI的卡顿,甚至受到默认的RunLoopModel的影响,我们在滑动tableview的时候,会导致时间的处理停止。
  • 这里时候我们就需要一个子线程来处理事件和网络请求的回调了。但是,子线程在处理完事件后就会自动结束生命周期,这个时候后面的一些网络请求得回调我们就无法接收了。所以我们就需要开启子线程的RunLoop来保存线程的常驻

KVO的使用?实现原理?

KVO(key-value-observing) 即键值观察,利用一个key来找到某个属性并监听其值的改变。

使用步骤:
  1. 添加观察者
  2. 在观察者中实现监听方法,observeValueForKeyPath: ofObject: change: context:(通过查阅文档可以知道,绝大多数对象都有这个方法,因为这个方法属于NSObject)
  3. 移除观察者
原理:

1626659636976.jpg

  1. 创建派生类

    当某个类的对象第一次被观察时,系统就会在运行期动态地创建该类的一个派生类,在这个派生类中重写基类中任何被观察属性的 setter 方法。

  2. 重写Setter方法

    重写的 setter 方法,触发设置属性 会调用 setter 方法,获得 KVO 需要的通知机制。当然前提是要通过遵循 KVO 的属性设置方式来变更属性值,如果仅是直接修改属性对应的成员变量,是无法实现 KVO 的。

  3. 改变isa指针指向

    重写setter方法,导致系统将这个对象的 isa 指针指向这个新的派生类,因此这个对象就成为该派生类的对象了。

    从而激活了键值通知机制。

    此外,派生类还重写了 dealloc 方法来释放资源。


KVC的使用?实现原理?

KVC(key-Value coding)即键值编码。指iOS开发中,可以允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值。不需要调用明确的存取方法,这样就可以在运行时动态访问和修改对象的属性,而不是在编译时确定。

使用:
  • 动态地取值和设值

  • 用KVC来访问和修改私有变量

    [obj setValue:@"xxx" forKeyPath:@"_height"];
    
  • Model和字典转换

    NSDictionary *dic = @{@"name":@"book", @"num" : @"66", @"id":@"123"};
    Model *model = [[Model alloc] init]; 
    [model setValuesForKeysWithDictionary:dic]; // 字典转model
    NSDictionary *modelDic = [model dictionaryWithValuesForKeys:@[@"name",@"num",@"goodId"]];// model转字典
    
  • 修改一些控件的内部属性

  • 操作集合

原理:

当一个对象调用setValue方法时,方法内部会做以下操作:

  1. 检查是否存在相应的key的setter方法。
  2. 查找与key相同名称并且带下划线的成员变量。
  3. 查找相同名称的属性key。
  4. 最终还没找到,就调用valueForUndefinedKey:和setValue:forUndefinedKey:方法。

当一个对象调用getValue方法时,方法内部会做以下操作:

  1. 检查是否存在相应的key的getter方法。
  2. 查找countOfKeyobjectInKeyAtIndexkeyAtIndexes格式的方法。返回一个可以响应NSArray所有方法的代理集合。
  3. 查找countOfKeyenumeratorOfKeymemberOfKey格式的方法。返回一个可以响应NSSet所有方法的代理集合。
  4. 最终还没找到,就调用valueForUndefinedKey:方法。