IOS高级面试题

作者: 小明2021 | 来源:发表于2017-12-09 16:44 被阅读180次

    Tip: 自己开发了好玩的APP: 《灵感胶囊》(App Store上搜索:"灵感胶囊")

    点击下载 "灵感胶囊"
    灵感胶囊 - 致力于做最简单便捷的备忘录
    灵感胶囊 - 把你任何时候的想法,随手、随口记录下来
    灵感胶囊 - 没有多余的交互,没有复杂的逻辑,只有对用户更简单便捷的操作
    灵感胶囊 - 提供小组件的快捷方式,方便添加文字和语音灵感
    灵感胶囊 - 提供针对某一个灵感加密
    灵感胶囊 - 增加炫酷的黑色版
    灵感胶囊 - 我们会一直优化下去,功能会越来越多,越来越方便
    灵感胶囊 - 让您办事更加高效
    您可以在意见反馈里提意见,也可以加我的扣扣
    灵感胶囊官方QQ : 1049055935
    灵感胶囊官方博客:https://www.jianshu.com/u/4014bc9df991

    一:谈谈离屏渲染

    1、GPU渲染机制:

    CPU 计算好显示内容提交到 GPU,GPU 渲染完成后将渲染结果放入帧缓冲区,随后视频控制器会按照 VSync 信号逐行读取帧缓冲区的数据,经过可能的数模转换传递给显示器显示。
    GPU屏幕渲染分两种方式:
    1、On-Screen Rendering:意为当前屏幕渲染,指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区中进行。
    2、Off-Screen Rendering:意为离屏渲染,指的是GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。

    2、CPU渲染机制:

    如果我们重写了drawRect方法,并且使用任何Core Graphics的技术进行了绘制操作,就涉及到了CPU渲染。整个渲染过程由CPU在App内 同步地
    完成,渲染得到的bitmap最后再交由GPU用于显示。
    CoreGraphic通常是线程安全的,所以可以进行异步绘制,显示的时候再放回主线程,一个简单的异步绘制过程大致如下:

    - (void)display {
         dispatch_async(backgroundQueue, ^{
             CGContextRef ctx = CGBitmapContextCreate(...);
             // draw in context...
             CGImageRef img = CGBitmapContextCreateImage(ctx);
             CFRelease(ctx);
             dispatch_async(mainQueue, ^{
                 layer.contents = img;
             });
         });
      }
    
    3、离屏渲染的触发方式:

    shouldRasterize(光栅化)、masks(遮罩)、shadows(阴影)、edge antialiasing(抗锯齿)、group opacity(不透明)、复杂形状设置圆角等、渐变(其中shouldRasterize(光栅化)是比较特别的一种:
    光栅化概念:将图转化为一个个栅格组成的图象。
    光栅化特点:每个元素对应帧缓冲区中的一像素)

    4、为什么要使用离屏渲染:

    当使用圆角,阴影,遮罩的时候,图层属性的混合体被指定为在未预合成之前不能直接在屏幕中绘制,所以就需要屏幕外渲染被唤起。

    屏幕外渲染并不意味着软件绘制,但是它意味着图层必须在被显示之前在一个屏幕外上下文中被渲染(不论CPU还是GPU)。

    所以当使用离屏渲染的时候会很容易造成性能消耗,因为在OPENGL里离屏渲染会单独在内存中创建一个屏幕外缓冲区并进行渲染,而屏幕外缓冲区跟当前屏幕缓冲区上下文切换是很耗性能的。

    5、Instruments监测离屏渲染:

    Color Offscreen-Rendered Yellow
    开启后会把那些需要离屏渲染的图层高亮成黄色,这就意味着黄色图层可能存在性能问题。

    Color Hits Green and Misses Red
    如果shouldRasterize被设置成YES,对应的渲染结果会被缓存,如果图层是绿色,就表示这些缓存被复用;如果是红色就表示缓存会被重复创建,这就表示该处存在性能问题了。

    5、离屏渲染的解决方案:

    1、圆角的优化:
    方案一:(ios9以后系统做了优化,不会产品离屏渲染,但是不建议用)

    iv.layer.cornerRadius = 30;
    iv.layer.masksToBounds = YES;
    

    方案二:(利用mask设置圆角,利用的是UIBezierPath和CAShapeLayer来完成)

    UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(100, 100, 100, 100)];
        imageView.image = [UIImage imageNamed:@"1"];
        UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:imageView.bounds byRoundingCorners:UIRectCornerAllCorners cornerRadii:imageView.bounds.size];
        
        CAShapeLayer *maskLayer = [[CAShapeLayer alloc]init];
        //设置大小
        maskLayer.frame = imageView.bounds;
        //设置图形样子
        maskLayer.path = maskPath.CGPath;
        imageView.layer.mask = maskLayer;
        [self.view addSubview:imageView];
    

    方案三:(利用CoreGraphics画一个圆形上下文,然后把图片绘制上去,得到一个圆形的图片,达到切圆角的目的。)

    - (UIImage *)drawCircleImage:(UIImage*)image
    {
        CGFloat side = MIN(image.size.width, image.size.height);
        
        UIGraphicsBeginImageContextWithOptions(CGSizeMake(side, side), false, [UIScreen mainScreen].scale);
        CGContextAddPath(UIGraphicsGetCurrentContext(), [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, side, side)].CGPath);
        CGContextClip(UIGraphicsGetCurrentContext());
        
        CGFloat marginX = -(image.size.width - side) * 0.5;
        CGFloat marginY = -(image.size.height - side) * 0.5;
        [image drawInRect:CGRectMake(marginX, marginY, image.size.width, image.size.height)];
        
        CGContextDrawPath(UIGraphicsGetCurrentContext(), kCGPathFillStroke);
        
        UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        
        return newImage;
    }
    
    
    6、三种方案的优缺点:

    使用drawRect有什么影响?
    drawRect方法依赖Core Graphics框架来进行自定义的绘制
    缺点:它处理touch事件时每次按钮被点击后,都会用setNeddsDisplay进行强制重绘;而且不止一次,每次单点事件触发两次执行。这样的话从性能的角度来说,对CPU和内存来说都是欠佳的。特别是如果在我们的界面上有多个这样的UIButton实例,那就会很糟糕了
    这个方法的调用机制也是非常特别. 当你调用 setNeedsDisplay 方法时, UIKit 将会把当前图层标记为dirty,但还是会显示原来的内容,直到下一次的视图渲染周期,才会将标记为 dirty 的图层重新建立Core Graphics上下文,然后将内存中的数据恢复出来, 再使用 CGContextRef 进行绘制

    二:谈谈RunLoop

    1、Runloop的概念:

    RunLoop系统中和线程相关的基础架构的组成部分(和线程相关),一个RunLoop是一个事件处理环,系统利用这个事件处理环来安排事务,协调输入的各种事件。RunLoop的目的是让你的线程在有工作的时候忙碌,没有工作的时候休眠(和线程相关)。可能这样说你还不是特别清楚RunLoop究竟是用来做什么的,打个比方来说明:我们把线程比作一辆跑车,把这辆跑车的主人比作RunLoop,那么在没有'主人'的时候,这个跑车的生命是直线型的,其启动,运行完之后就会废弃(没有人对其进行控制,'撞坏'被收回),当有了RunLoop这个主人之后,‘线程’这辆跑车的生命就有了保障,这个时候,跑车的生命是环形的,并且在主人有比赛任务的时候就会被RunLoop这个主人所唤醒,在没有任务的时候可以休眠(在IOS中,开启线程是很消耗性能的,开启主线程要消耗1M内存,开启一个后台线程需要消耗512k内存,我们应当在线程没有任务的时候休眠,来释放所占用的资源,以便CPU进行更加高效的工作),这样可以增加跑车的效率,也就是说RunLoop是为线程所服务的。这个例子有点不是很贴切,线程和RunLoop之间是以键值对的形式一一对应的,其中key是thread,value是runLoop(这点可以从苹果公开的源码中看出来)其实RunLoop是管理线程的一种机制,这种机制不仅在IOS上有,在Node.js中的EventLoop,Android中的Looper,都有类似的模式。刚才所说的比赛任务就是唤醒跑车这个线程的一个source;RunLoop Mode就是,一系列输入的source,timer以及observerRunLoop Mode包含以下几种: NSDefaultRunLoopMode,NSEventTrackingRunLoopMode,UIInitializationRunLoopMode,NSRunLoopCommonModes,NSConnectionReplyMode,NSModalPanelRunLoopMode
    同步执行的话并没有开启新线程,而runloop和线程是联系在一起的

    2、RunLoop应用:

    NSTimer、PerformSelector、常驻线程(某些操作,需要重复开辟子线程,重复开辟内存过于消耗性能,可以设定子线程常驻)、自动释放池(创建和释放
    1.第一次创建, 是在runloop进入的时候创建,对应的状态 = kCFRunLoopEntry
    2.最后一次释放, 是在runloop退出的时候 对应的状态 = kCFRunLoopExit
    3.其它创建和释放
    每次睡觉的时候都会释放前自动释放池,然后再创建一个新的)、可以添加Observer监听RunLoop的状态:比如监听点击事件的处理(在所有点击事件之前做一些事情)
    子线程RunLoop常驻:

    // 1.子线程的NSRunLoop需要手动创建
        // 2.子线程的NSRunLoop需要手动开启
        // 3.如果子线程的NSRunLoop没有设置source or timer, 那么子线程的NSRunLoop会立刻关闭
        // 无含义,设置子线程为常住线程,让子线程不关闭
        // [[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
    
        NSTimer *timer = [NSTimer timerWithTimeInterval:5.0 target:self selector:@selector(test) userInfo:nil repeats:YES];
        // 会添加到当前子线程
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
        [[NSRunLoop currentRunLoop] run];
    
     注意
        - NSRunLoop只会检查有没有source和timer, 没有就关闭, 不会检查observer
        - 主线程没有到期时间,子线程有
    
    
    3、NSTimer的理解

    NSTimer在主线程执行默认是放到主线程的runloop里面的,在子线程必须手动加一个Runloop才可以。有时候我们会在这个线程中执行一个耗时操作,这个时候RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer,这就造成了误差(Timer有个冗余度属性叫做tolerance,它标明了当前点到后,容许有多少最大误差),可以在执行一段循环之后调用一个耗时操作,很容易看到timer会有很大的误差,这说明在线程很闲的时候使用NSTiemr是比较傲你准确的,当线程很忙碌时候会有较大的误差。系统还有一个CADisplayLink,也可以实现定时效果,它是一个和屏幕的刷新率一样的定时器。如果在两次屏幕刷新之间执行一个耗时的任务,那其中就会有一个帧被跳过去,造成界面卡顿。另外GCD也可以实现定时器的效果,由于其和RunLoop没有关联,所以有时候使用它会更加的准确,
    避免NSTimer循环引用的问题,简单需求可以在viewWillappear里面创建,在viewWillDisappear里面移除。
    复杂的需求需要自己写了:
    NSTimer 已知是会强引用参数 target:self 的了,如果忘记关 timer 的话,传什么进去都会被强引用。干脆实现一个 timer 算了,timer 的功能就是定时调某个方法,NSTimer 的调用时间是不精确的!它挂在 runloop 上受线程切换,上一个事件执行时间的影响。

    利用 dispatch_asyn() 定时执行函数。看下面代码。

    - (void)loop {
        [self doSomething];
        ......
        // 休息 time 秒,再调 loop,实现定时调用
        [NSThread sleepForTimeInterval:time];
        dispatch_async(self.runQueue, ^{
            [weakSelf loop];
        });    
    }
    

    dispatch_async 中调 loop 不会产生递归调用

    dispatch_async 是在队列中添加一个任务,由 GCD 去回调 [weakSelf loop]

    这办法解决了timer 不能释放,挂在 runloop 不能移除的问题。

    利用这方法,我写了个不会发生循环引用的 timer,controller 释放,timer 也自动停止释放,甚至 timer 的 block 里面可以直接写 self,也不会循环引用。

    4、线程和RunLoop的关系:

    Run loop,正如其名,loop表示某种循环,和run放在一起就表示一直在运行着的循环。实际上,run loop和线程是紧密相连的,可以这样说run loop是为了线程而生,没有线程,它就没有存在的必要。Run loops是线程的基础架构部分,Cocoa和CoreFundation都提供了run loop对象方便配置和管理线程的run loop(以下都已Cocoa为例)。每个线程,包括程序的主线程(main thread)都有与之相应的run loop对象。
    重点是UIApplicationMain() 函数,这个方法会为main thread 设置一个NSRunLoop 对象,这就解释了本文开始说的为什么我们的应用可以在无人操作的时候休息,需要让它干活的时候又能立马响应。
    Cocoa中的NSRunLoop类并不是线程安全的
    我们不能再一个线程中去操作另外一个线程的run loop对象,那很可能会造成意想不到的后果。不过幸运的是CoreFundation中的不透明类CFRunLoopRef是线程安全的,而且两种类型的run loop完全可以混合使用。Cocoa中的NSRunLoop类可以通过实例方法:

    • (CFRunLoopRef)getCFRunLoop;
      获取对应的CFRunLoopRef类,来达到线程安全的目的。
      Run loop接收输入事件来自两种不同的来源:输入源(input source)和定时源(timer source)。两种源都使用程序的某一特定的处理例程来处理到达的事件。

    三、 APP启动时间优化

    一般而言,启动时间是指从用户点击 APP 那一刻开始到用户看到第一个界面这中间的时间。我们进行优化的时候,我们将启动时间分为 pre-main 时间和 main 函数到第一个界面渲染完成时间这两个部分。
    1、其实 didFinishLaunchingWithOptions 方法里我们一般都有以下的逻辑
    初始化第三方 SDK
    配置 APP 运行需要的环境
    自己的一些工具类的初始化
    一个可怕的事情是:可怕的一件事情,为什么呢?因为一般我们都把界面的初始化、网络请求、数据解析、视图渲染等操作放在了 viewDidLoad 方法里,这样一来每次启动 APP 的时候,在用户看到第一个页面之前,我们要把这些事件全部都处理完,才会进入到视图渲染阶段。
    1、日志、统计等必须在 APP 一起动就最先配置的事件
    2、项目配置、环境配置、用户信息的初始化 、推送、IM等事件
    3、其他 SDK 和配置事件
    可以把不必须的操作从viewDidload里面移动到viewwillappear。

    上面已经将 t2 时间处理好了,接下来看看 pre-main。

    苹果为查看 pre-main 提供了支持,具体配置如下,配置的 key 为:DYLD_PRINT_STATISTICS
    Run - Arguments - Environment Variables 添加 DYLD_PRINT_STATISTICS = YES
    Run - Diagnostics - Dynamic Library Loads 勾选
    然后再运行项目,Xcode 就会在控制台输出这部分 pre-main 的耗时:

    Total pre-main time: 2.2 seconds (100.0%)
    dylib loading time: 1.0 seconds (45.2%)
    rebase/binding time: 100.05 milliseconds (4.3%)
    ObjC setup time: 207.21 milliseconds (9.0%)
    initializer time: 946.39 milliseconds (41.3%)
    slowest intializers :
    libSystem.B.dylib : 8.54 milliseconds (0.3%)
    libBacktraceRecording.dylib : 46.30 milliseconds (2.0%)
    libglInterpose.dylib : 187.42 milliseconds (8.1%)
    beiliao : 896.56 milliseconds (39.1%)

    但是这部分不是那么好处理,因为这部分主要是由以下几个方面影响的:

    用到的系统的动态库的数量,比如 UIKit.framework 等
    cocoapods 里引用的第三方框架数量
    项目中类的数量
    load 方法中执行的代码
    组件化

    :iOS动态库和静态库的区别:

    异同点:

    静态库:链接时完整地拷贝至可执行文件中,被多次使用就有多份冗余拷贝。

    动态库:链接时不复制,程序运行时由系统动态加载到内存,供程序调用,系统只加载一次,多个程序可以共用,节省内存。

    共同点:

    静态库和动态库都是闭源库,只能拿来满足某个功能的使用,不会暴露内部具体的代码信息,而从github上下载的第三方库大多是开源库

    3.这两种库都有哪些文件格式?

    静态库:.a和.framework

    动态库:.dylib和.framework(系统直接提供给我们的framework都是动态库!)
    2.当你创建一个framework文件时,系统“默认”是一个动态库的格式,如果想做成静态库,需要在buildSetting中将Mach-O Type选项设置为Static Library就行了!

    .a文件是一个纯二进制文件,不能直接拿来使用,需要配合头文件、资源文件一起使用。

    将静态库打包的时候,只能打包代码资源,但是图片文件、本地json文件和xib等资源文件无法打包进去,使用.a静态库的时候需要三个组成部分:.a文件+需要暴露的头文件+资源文件;

    .framework文件内部除了有二进制文件(如下图黑色文件)之外还有其他的资源文件(相当于:.framwork文件=黑色二进制文件<.a文件+.h文件>+资源文件<图片、以及本地的html5,json,plist等),可以直接拿来在工程中使用。

    5.制作静态库时需要注意的几点:

    (1)图片资源的处理:两种格式的静态库,一般都是把图片文件单独的放在一个.bundle文件中,一般.bundle的名字和.a或.framework的名字相同。(.bundle文件很好弄,在桌面上新建一个文件夹,把它重命名为XXX.bundle就可以了(选中文件->右键->显示包内容->拖拽添加图片资源))。

    (2)category是我们实际开发项目中经常用到的,把category打成静态库是没有问题的,但是在使用这个静态库的工程中,调用category中的方法时,会出现找不到该方法的运行时错误:selector not recognized,解决办法是:在使用静态库的工程中配置other linkerflags的值为-ObjC。

    (3)如果一个静态库很复杂,需要暴露的.h比较多的话,就可以在静态库的内部创建一个.h文件(一般这个.h文件的名字和静态库的名字相同),然后把所有需要暴露出来的.h文件都集中放在这个.h文件中,而那些原本需要暴露的.h都不需要再暴露了,只需要把.h暴露出来就可以了。

    6.framework动态库的主要作用:

    framework本来是苹果专属的内部提供的动态库文件格式,但是自从2014年WWDC之后,开发者也可以自定义创建framework实现动态更新(绕过apple store审核,从服务器发布更新版本)的功能,这与苹果限定的上架的app必须经过apple store的审核制度是冲突的,所以含有自定义的framework的app是无法在商店上架的,但是如果开发的是企业内部应用,就可以考虑尝试使用动态更新技术来将多个独立的app或者功能模块集成在一个app上面!(我开发的就是企业内部使用的app,我们将企业官网中的板块开发成4个独立的app,然后将其改造为framework文件最终集成在一款平台级的app当中进行使用,这样就可以在一款app上面使用原本4个app的全部功能!)

    7.iOS 如何使用 framework 来进行动态更新!

    四:消息传递:

    在iOS开发中经常会遇到unrecognized selector sent to instance 0x100111df0'的问题,这是为什么呢,从字面上理解来说是无法识别的selector子发送给对象,其实调用一个不存在的方法就会遇到这个问题。
    严格来说iOS中不存在方法调用的说法,应该说是消息的传递。

    消息传递和函数调用的区别就是,你可以在任意的时候对一个对象发送任何消息,而不需要在编译的时候声明。但是函数调用就不行。

    判断receiver是否为nil,如果是nil的话则不往下执行,返回nil,这就是为什么在oc中一个nil发送消息不会引起奔溃。
    1、从方法的缓存中查找 被调用过的方法会存在缓存里面,每个类都会有一个表来存被调用过的方法,以便下次更快的调用。
    2、从本类的方法表(dispatch table)中查找方法寻找selector,找到则写入缓存,返回方法。否则再从父类中查找方法,如此往复,直到达到基类。如果找不到则执行方法的动态解析。
    3、方法的动态解析: 调用 + (BOOL)resolveInstanceMethod:(SEL)sel方法来查看是否能够返回一个selector,如果存在则返回selector。不存在进入下一步。
    4、备用接受者 - (id)forwardingTargetForSelector:(SEL)aSelector这个方法来询问是否有接受者可以接受这个方法呀。如果有人接受,则交给它处理,就好像一切都没发生过一样。
    5、方法的转发: 如果到这一步还不能够找到相应的Selector的话,就要进行完整的方法转发过程。调用方法(void)forwardInvocation:(NSInvocation *)anInvocation
    最后还是没有找到的话就只有呵呵了,这时候unrecognized selector sent to instance 0x100111df0'的错误就来了。

    这里可以看到查找一个方法需要经过很多的步骤,所以我们很多次机会来弥补这种错误,但是越往后面处理消息所消耗的代价越大。我们从第一步开始看,最好能够在一开始就找到相应的selector,那么他就会把方法缓存起来,等再次调用相同的方法的时候就会直接从缓存中取出来,那效率很高,和直接用c调用的速度慢不了多少。在没有缓存的情况下会从类的方法表里面进行查找。一个对象会有一个isa指针来指向自己所属的类。而类则会有一个方法表(dispatch table),用于将selector和真正实现的内存地址对应起来。另外还有一个指针会指向父类,这样就可以逐级向上查找直到基类

    方法的动态解析

    • (instancetype)init {

      if (self = [super init]) {
      [self performSelector:@selector(creash)];

    }
    return self;
    

    }
    这里我调用了creash,但是方法并没有被实现,所以会出错。
    我们来实现下面的方法,不要忘记导入头文件#import <objc/runtime.h>

    + (BOOL)resolveInstanceMethod:(SEL)sel {
    
        NSString *selectorString = NSStringFromSelector(sel);
        if ([selectorString isEqualToString:@"creash"]) {
            class_addMethod(self,
                            sel,
                            (IMP)askMeWhenCreash,
                            "");
            
            return YES;
        }
        return NO;
    }
    
    void askMeWhenCreash() {
        NSLog(@"creash不要慌,来执行这个");
    }
    

    在creash方法没找到之后,程序首先进入resolveInstanceMethod方法,我们先来判断方法名是否为creash,如果是的话我们在这里用class_addMethod(Class cls, SEL name, IMP imp, const char *types)方法动态的给他添加方法的实现。第三个参数imp就是,我们将它设为自己定义的一个方法void askMeWhenCreash(),最后return YES表示我们已经处理,不会再报错。

    消息转发:

    还是上面那个例子,我们继续调用
    
    [self performSelector:@selector(testForward:) withObject:@"arg1sdfsdfsdf"];
    要使用消息的转发必须要覆盖两个方法在methodSignatureForSelector和forwardInvocation
    前者永辉为方法创建一个有效的签名。必须实现。
    
    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
    
        return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
    }
    
    - (void)forwardInvocation:(NSInvocation *)anInvocation {
    
        [anInvocation setSelector:@selector(forwardTo:)];
        NSString *arg1;
        [anInvocation getArgument:&arg1 atIndex:2];
        [anInvocation invokeWithTarget:self];
    }
    
    - (void)forwardTo:(NSString *)arg1 {
    
        NSLog(@"%@",arg1);
    }
    
    输出
    2015-08-21 15:23:37.560 objc_msgSendTest[18793:1974024] arg1sdfsdfsdf
    
    这里我们把未实现的testForward方法转发到了(void)forwardTo:(NSString *)arg1方法上去
    
    上面有一个小问题就是关于参数的问题,明明只有一个参数为什么Index为2呢,这是因为在objective-C中的方法默认隐藏了两个参数,self和_cmd。这样说的话就很容易来解释方法签名中的"v@:@"是什么鬼,v表示返回值void,接下来就是三个参数。
    
    

    TableView的流程优化:

    1.提前计算并缓存好高度,因为heightForRow最频繁的调用。

    2.异步绘制,遇到复杂界面,性能瓶颈时,可能是突破口。

    3.滑动时按需加载,这个在大量图片展示,网络加载时,很管用。(SDWebImage已经实现异步加载)。

    4.重用cells。

    5.如果cell内显示得内容来自web,使用异步加载,缓存结果请求。

    6.少用或不用透明图层,使用不透明视图。

    7.尽量使所有的view opaque,包括cell本身。

    8.减少subViews

    9.少用addView给cell动态添加view,可以初始化的时候就添加,然后通过hide控制是否显示。

    做到前几点后,你的table view滚动时应该足够流畅了,不过你仍可能让用户感到不爽。常见的现象就是在更新数据时,整个界面卡住不动,完全不响应用户请求。

    出现这种现象的原因就是主线程执行了耗时很长的函数或方法,在其执行完毕前,无法绘制屏幕和响应用户请求。其中最常见的就是网络请求了,它通常都需要花费数秒的时间,而你不应该让用户等待那么久。

    解 决办法就是使用多线程,让子线程去执行这些函数或方法。这里面还有一个学问,当下载线程数超过2时,会显著影响主线程的性能。因此在使用 ASIHTTPRequest时,可以用一个NSOperationQueue来维护下载请求,并将其 maxConcurrentOperationCount设为2。而NSURLRequest则可以配合GCD来实现,或者使用NSURLConnection的setDelegateQueue:方法。

    1 - (void)scrollViewDidEndDragging:(UIScrollView *)scrollView willDecelerate:(BOOL)decelerate {
     if (!decelerate) { queue.maxConcurrentOperationCount = 5; } 
    }
     - (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { queue.maxConcurrentOperationCount = 5; } 
    - (void)scrollViewWillBeginDragging:(UIScrollView *)scrollView { 
    queue.maxConcurrentOperationCount = 2; } 
    

    此外,自动载入更新数据对用户来说也很友好,这减少了用户等待下载的时间。例如每次载入50条信息,那就可以在滚动到倒数第10条以内时,加载更多信息:

    • (void)tableView:(UITableView *)tableView willDisplayCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath { if (count - indexPath.row < 10 && !updating) { updating = YES; [self update]; } }// update方法获取到结果后,设置updating为NO
      还有一点要注意的就是当图片下载完成后,如果cell是可见的,还需要更新图像:

    1 NSArray *indexPaths = [self.tableView indexPathsForVisibleRows];
    2 for (NSIndexPath *visibleIndexPath in indexPaths) {
    3 if (indexPath == visibleIndexPath) {
    4 MyTableViewCell *cell = (MyTableViewCell *)[self.tableView cellForRowAtIndexPath:indexPath];
    5 cell.image = image;
    6 [cell setNeedsDisplayInRect:imageRect]; break;
    7 }
    8 }// 也可不遍历,直接与头尾相比较,看是否在中间即可。

    最后还是前面所说过的insertRowsAtIndexPaths:withRowAnimation:方法,插入新行需要在主线程执行,而一次插入很多行的话(例如50行),会长时间阻塞主线程。而换成reloadData方法的话,瞬间就处理完了。

    YYKit学习

    <pre>

    • YYModel — 高性能的 iOS JSON 模型框架。
    • YYCache — 高性能的 iOS 缓存框架。
    • YYImage — 功能强大的 iOS 图像框架。
    • YYWebImage — 高性能的 iOS 异步图像加载框架。
    • YYText — 功能强大的 iOS 富文本框架。
    • YYKeyboardManager — iOS 键盘监听管理工具。
    • YYDispatchQueuePool — iOS 全局并发队列管理工具。
    • YYAsyncLayer — iOS 异步绘制与显示的工具。
    • YYCategories — 功能丰富的 Category 类型工具库。

    Tip:
    1、缓存
    Model JSON 转换过程中需要很多类的元数据,如果数据足够小,则全部缓存到内存中。
    2、查表
    当遇到多项选择的条件时,要尽量使用查表法实现,比如 switch/case,C Array,如果查表条件是对象,则可以用 NSDictionary 来实现。
    3、避免 KVC
    Key-Value Coding 使用起来非常方便,但性能上要差于直接调用 Getter/Setter,所以如果能避免 KVC 而用 Getter/Setter 代替,性能会有较大提升。
    4、避免 Getter/Setter 调用
    如果能直接访问 ivar,则尽量使用 ivar 而不要使用 Getter/Setter 这样也能节省一部分开销。
    5、避免多余的内存管理方法
    在 ARC 条件下,默认声明的对象是 __strong 类型的,赋值时有可能会产生 retain/release 调用,如果一个变量在其生命周期内不会被释放,则使用 __unsafe_unretained 会节省很大的开销。
    访问具有 __weak 属性的变量时,实际上会调用 objc_loadWeak() 和 objc_storeWeak() 来完成,这也会带来很大的开销,所以要避免使用 __weak 属性。
    创建和使用对象时,要尽量避免对象进入 autoreleasepool,以避免额外的资源开销。
    6、遍历容器类时,选择更高效的方法
    相对于 Foundation 的方法来说,CoreFoundation 的方法有更高的性能,用 CFArrayApplyFunction() 和 CFDictionaryApplyFunction() 方法来遍历容器类能带来不少性能提升,但代码写起来会非常麻烦。

    7、尽量用纯 C 函数、内联函数
    使用纯 C 函数可以避免 ObjC 的消息发送带来的开销。如果 C 函数比较小,使用 inline 可以避免一部分压栈弹栈等函数调用的开销。
    8、减少遍历的循环次数
    在 JSON 和 Model 转换前,Model 的属性个数和 JSON 的属性个数都是已知的,这时选择数量较少的那一方进行遍历,会节省很多时间。
    YYModel的特性

    高性能: 模型转换性能接近手写解析代码。
    自动类型转换: 对象类型可以自动转换,详情见下方表格。
    类型安全: 转换过程中,所有的数据类型都会被检测一遍,以保证类型安全,避免崩溃问题。
    无侵入性: 模型无需继承自其他基类。
    轻量: 该框架只有 5 个文件 (包括.h文件)。
    文档和单元测试: 文档覆盖率100%, 代码覆盖率99.6%。

    相关文章

      网友评论

        本文标题:IOS高级面试题

        本文链接:https://www.haomeiwen.com/subject/nfuuixtx.html