OC

日常积累

Posted by sunzhongliang on May 23, 2018

程序启动流程

  • pre main阶段(操作系统开始执行一个可执行文件,并完成进程创建、执行文件加载、动态链接、环境配置)
    1. Mach-O可执行文件加载
    2. dyld 加载程序所需的动态库。
    3. rebase(偏移修正)
      • 系统会随机分配ASLR(地址空间布局随机化), 例如,二进制文件中有一个test方法,偏移值是0x0001,而随机分配的ASLR是0x1f00,如果想访问test方法,其内存地址(即真实地址)变为 ASLR+偏移值 = 运行时确定的内存地址(即0x1f00+0x0001 = 0x1f01
    4. binding
      • 加载类扩展(Category)中的方法
      • C++静态对象加载、调用ObjC的 +load 函数
      • 执行声明为__attribute__((constructor))的C函数
  • main
    1. dyld调用main()
    2. 调用UIApplicationMain()
    3. 调用applicationWillFinishLaunching
    4. 调用didFinishLaunchingWithOptions

程序启动优化

pre-main

  1. 减少方法、分类(category)的数量
  2. 减少c++虚函数的数量(创建虚函数有开销)
  3. 减少objc类的数量
  4. 减少+(load)数量,尽量推迟到+(initiailize)方法

main

  1. 减少didFinishLaunchingWithOptions要做的事情
  2. 有的操作推迟到UIWindow的RootViewController视图的ViewDidLoad之后执行。
  3. 避免复杂/多余的计算
  4. 避免RootViewController的ViewDidLoad方法做太多事情
  5. 能够延迟、异步加载的就延迟、异步加载

冷启动数据监控

  1. 结束时间点: 冷启动的结束时间点比较好确定,我们一般以首页的viewDidLoad结束作为统计时间
  2. 开始时间点: 一般情况下,我们是在main()函数之后才开始接管APP的,但以main()函数作为统计开始时间点显然不合适,因为没有统计到pre-main的时间,目前业界有最常见两种方法:
    • 以可执行文件中的一个类的+load方法作为统计开始时间点
    • 分析dylib的依赖关系,找到最底层的dylib,然后以其中某个类的+load方法作为开始时间点 但这两种统计都不太好,目前我们可以通过sysctl函数获得进程的有关信息,其中就包括进程创建的时间戳
 #import <sys/sysctl.h> 
 #import <mach/mach.h>

+ (BOOL)processInfoForPID:(int)pid procInfo:(struct kinfo_proc*)procInfo {
    int cmd[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, pid};
    size_t size = sizeof(*procInfo);
    return sysctl(cmd, sizeof(cmd)/sizeof(*cmd), procInfo, &size, NULL, 0) == 0;
}

+ (NSTimeInterval)processStartTime
{
    struct kinfo_proc kProcInfo;
    if ([self processInfoForPID:[[NSProcessInfo processInfo] processIdentifier] procInfo:&kProcInfo]) {
        return kProcInfo.kp_proc.p_un.__p_starttime.tv_sec * 1000.0 + kProcInfo.kp_proc.p_un.__p_starttime.tv_usec / 1000.0; } 
    else {
        NSAssert(NO, @" ");
        return 0; 
    }
}

列表视图性能优化

  1. 无事件的视图可用CALayer替代(但需要注意有一个隐式动画和scale的坑)
  2. 减少View的层级,少用透明图层,少用AutoLayout
  3. 如果对象不涉及UI操作,尽量放在后台线程完成
  4. 减少离屏渲染,border、圆角、阴影、遮罩,masktobounce
  5. 缓存高度
    • AutoLayout:比较常见的优化方案有UITableView-FDTemplateLayoutCell;原理:在heightForRow方法里使用 systemLayoutSizeFittingSize 获取高度,并使用二维数组将高度缓存下来,然后利用RunTime Hook住tableView的reload、delete等方法,去操作二维数组里面缓存的高度值
    • 手写布局:后台线程提前计算好视图布局,并对视图布局进行缓存。可异步调用 [NSAttributedString boundingRectWithSize:options:context:] 来计算文本宽高;异步调用 -[NSAttributedString drawWithRect:options:context:] 来绘制文本。需要放到后台线程进行,以避免阻塞主线程;更加好的方式是使用CoreText在异步线程进行排版,排版对象也可保存下来,然后在主线程展示
  6. CALayer 内部并没有属性,当调用属性方法时,它内部是通过运行时 resolveInstanceMethod 为对象临时添加一个方法,并把对应属性值保存到内部的一个 Dictionary 里,同时还会通知 delegate、创建动画等等,非常消耗资源。UIView 的关于显示相关的属性(比如 frame/bounds/transform)等实际上都是 CALayer 属性映射来的,所以对 UIView 的这些属性进行调整时,消耗的资源要远大于一般的属性
  7. 文本渲染优化;屏幕上看到的文本内容控件,底层都是通过CoreText排版绘制为Bitmap显示,但都是在主线程进行。可通过自定义控件使用最底层的CoreText对文本内容异步绘制,CoreText对象创建好了之后可以直接获取对象的高度,还可以将CoreText对象缓存下来稍后渲染
  8. 图片的size最好跟UIImageView的size一致。若不一致CPU将会对图片做一个伸缩的操作,会浪费资源

离屏渲染

图形显示流程CPU计算好显示内容后提交到GPUGPU渲染完成后将渲染结果放入帧缓冲区,随后视频会按照VSync信号逐行读取帧缓冲区的内容,再传递给显示器显示。

iOS的是双缓冲区:
双缓冲区: 为了避免读取和刷新存在的效率问题,显示系统引入了双缓冲机制, 在这种情况下,GPU会预先渲染好一帧放入一个缓冲区,让视频控制器读取,当下一帧渲染好之后,GPU会直接把视频控制器的指针指向第二个缓冲区,这样效率会大大提升。
但仅仅是双缓冲机制,还存在另外的问题:当视频控制器还未读取完成时(屏幕内容显示一半),GPU将新的一帧内容提交到帧缓冲区,视频控制器就会把新的这帧数据显示到屏幕上,这样就会造成画面撕裂(显示效果不连贯一致)

iOS的VSync机制:
GPU通常有一个机制叫做垂直同步(简写也是 V-Sync), GPU会等待显示器的VSync信号发出后,才进行新的一帧渲染和缓冲区更新,这样就能解决画面撕裂现象。
VSync: 显示器是从左往右,从上到下逐行扫描,扫描完成后显示器就会呈现一帧画面,随后显示器的电子枪会复位继续下一次扫描;为了对显示过程和系统的视频控制器进行同步,当电子枪换到新一行时会发出一个水平同步信号(horizonal synchronization)HSync, 当一帧画面绘制完成后,电子枪恢复到原位准备下一帧时,显示器会发出一个垂直同步信号(vertical synchronization)VSync; 显示器通常以固定频率刷新,这刷新频率就是VSync信号产生的频率,这就是VSync的原理

卡顿原因
如果在一个 VSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。这就是界面卡顿的原因。

离屏渲染
当图层属性的混合体被指定为在未预合成之前不能直接在屏幕中绘制时屏幕外渲染就被唤起了, 需要再另开辟一个空间,用于临时渲染,渲染完成后再渲染到当前的缓冲区上,这个临时渲染,就是离屏渲染.
简单来说就是GPU无法一次性将图层渲染到缓冲区,而不得不另开一片内存,借助这个临时中转区来完成这个复杂的渲染

图片加载的性能优化

图片加载的流程
当调用imageWithNamed:系统只是在bundle内查找到文件名,然后把这个文件名放到UIImage里返回。图片并不是一赋值给imageView就显示的。图片需要在显示之前解压未压缩位图形式才能显示。当 UIImage 第一次显示到屏幕上时,其内部的解码方法才会被调用,同时解码结果会保存到一个全局缓存去。但是这样的一个操作是非常耗时的CPU操作,并且这个操作是在主线程当中进行的。所以如果没有特殊处理的情况下,在图片很多的列表里快速滑动的情况下会有性能问题,解决办法就是避免缓存:把图片用 CGContextDrawImage() 绘制到画布上,然后把画布的数据取出来当作图片。这种方式也是SDWebImage和YYWebImage的实现方式。具体解压缩的原理就是CGBitmapContextCreate方法重新生产一张位图然后把图片绘制当这个位图上,最后拿到的图片就是解压缩之后的图片。

图片的解码优化https://juejin.im/post/5adde71c6fb9a07aa63163eb, 其中讲到了处理特别大的图片时,用到了分块绘制,即把一张大图分割成若干小块,通过逐步绘制小块图片,最终生成大图

- (void)image
{
    UIImageView *imageView = [[UIImageView alloc] init];
    imageView.frame = CGRectMake(100, 100, 100, 56);
    [self.view addSubview:imageView];
    self.imageView = imageView;

    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        // 获取CGImage
        CGImageRef cgImage = [UIImage imageNamed:@"timg"].CGImage;

        // alphaInfo
        CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(cgImage) & kCGBitmapAlphaInfoMask;
        BOOL hasAlpha = NO;
        if (alphaInfo == kCGImageAlphaPremultipliedLast ||
            alphaInfo == kCGImageAlphaPremultipliedFirst ||
            alphaInfo == kCGImageAlphaLast ||
            alphaInfo == kCGImageAlphaFirst) {
            hasAlpha = YES;
        }

        // bitmapInfo
        CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
        bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;

        // size
        size_t width = CGImageGetWidth(cgImage);
        size_t height = CGImageGetHeight(cgImage);

        // context
        CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, CGColorSpaceCreateDeviceRGB(), bitmapInfo);

        // draw
        CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage);

        // get CGImage
        cgImage = CGBitmapContextCreateImage(context);

        // into UIImage
        UIImage *newImage = [UIImage imageWithCGImage:cgImage];

        // release
        CGContextRelease(context);
        CGImageRelease(cgImage);

        // back to the main thread
        dispatch_async(dispatch_get_main_queue(), ^{
            self.imageView.image = newImage;
        });
    });
}

事件的传递链以及响应链

事件的传递是从上到下的,事件的响应是从下到上的

在iOS中,由响应者链来对事件进行响应,所有事件响应的类都是UIResponder的子类, 发生事件时,事件首先被发送给第一响应者,第一响应者往往是事件发生的视图,然后事件将沿着响应者链一直向下传递,直到被接受并做出处理,如果整个过程都没有响应这个事件,该事件就被丢弃。

响应者链(Responder Chain)

响应者链指的是有响应和处理事件能力的对象。响应者链就是由一系列的响应者对象构成的一个层次结构,UIResponder是所有响应对象的基类
UIApplication、 UIViewController、UIWindow和所有继承自UIViewUIKit类都直接或间接的继承自UIResponder,所以它们的实例都是可以构成响应者链的响应者对象
响应者链的基本构成:

graph TD
    AppDelegate --> UIApplication --> UIWindow --> UIViewController --> View

事件分发

第一响应者(First responder)指的是当前接受触摸的响应者对象(通常是一个UIView对象),即表示当前该对象正在与用户交互,它是响应者链的开端。整个响应者链和事件分发的使命就是是找出第一响应者。

iOS系统检测到手指触摸(Touch)操作时会将其打包成一个UIEvent对象,并放入当前活动Application的事件队列,单例的UIApplication会从事件队列中取出触摸事件并传递给单例的UIWindow来处理,UIWindow对象首先会使用hitTest:withEvent:方法寻找此次Touch操作初始点所在的视图(View),即需要将触摸事件传递给其处理的视图,这个过程称之为hit-test view。

UIWindow实例对象会首先在它的内容视图上调用hitTest:withEvent:,此方法会在其视图层级结构中的每个视图上调用pointInside:withEvent:(该方法用来判断点击事件发生的位置是否处于当前视图范围内,以确定用户是不是点击了当前视图),如果pointInside:withEvent:返回YES,则继续逐级调用,直到找到touch操作发生的位置,这个视图也就是要找的hit-test view。

完整的事件响应处理流程

当手指触碰屏幕,屏幕感应到触碰后,将事件交由IOKit处理。
IOKit将触摸事件封装成一个IOHIDEvent对象,并通过mach port传递给SpringBoad进程。SpringBoard进程因接收到触摸事件,将触摸事件交给前台app进程来处理,APP进程的mach port接受到SpringBoard进程传递来的触摸事件,主线程的runloop被唤醒,触发了source1回调。
source1回调又触发了一个source0回调,将接收到的IOHIDEvent对象封装成UIEvent对象,此时APP将正式开始对于触摸事件的响应。
source0回调内部将触摸事件添加到UIApplication管理的一个事件队列当中,然后UIApplication将处于队列最前端的事件向下分发给UIWindow
UIWindow收到事件后再向下分发给UIView,UIView要先看看自己能不能处理事件(alpha不能为0、userInteractionEnabled不能为NO,hidden不能为YES),触摸点是否在自己身上(通过hitTest和pointInside),如果能那么继续寻找子视图,一直遍历如果都没有找到那么就不做任何事情

响应链的应用

更改一个对象的响应热区(比如我们通常给一个按钮增加触控区域),通过重写对象的pointInside方法

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event

实现一次点击的多次响应

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    NSLog(@"我是d我响应le");
    //调用下一响应者的响应方法
    [super touchesBegan:touches withEvent:event];
}

hitTest作用

hitTest作用是当一个事件传递给当前view时就会去调用,作用是去寻找最适合的View,返回的谁谁就去接收事件.
hitTest底层实现:
先看看自己能否接收事件,再看看触摸点是否在自己身上,从后往前遍历子控件,拿到子控件后,再次重复上面步骤,要把父控件上的坐标点转换为子控件坐标系下的点,再次执行hitTest方法,若是最后还没有找到合适的view,那么就return self,自己就是合适的view
当控件接收到触摸事件的时候,不管能不能处理事件,都会调用hitTest方法
hitTest:withEvent:方法的处理流程如下

  • 首先调用当前视图的pointInside:withEvent: 方法判断触摸点是否在当前视图内;
  • 若返回 NO, 则 hitTest:withEvent: 返回 nil,若返回 YES, 则向当前视图的所有子视图 (subviews) 发送 hitTest:withEvent: 消息,所有子视图的遍历顺序是从最顶层视图一直到到最底层视图,即从 subviews 数组的末尾向前遍历,直到有子视图返回非空对象或者全部子视图遍历完毕;
  • 若第一次有子视图返回非空对象,则 hitTest:withEvent: 方法返回此对象,处理结束;
  • 如所有子视图都返回空,则 hitTest:withEvent: 方法返回自身 (self)

程序卡顿监测

  1. 利用RunLoop监控状态,看两个状态之间(kCFRunLoopBeforeSources即将处理source/kCFRunLoopAfterWaiting休眠)的时间差,如果时间差比较久,可以立即把程序调用堆栈打印出来,就可以知道是哪个方法产生卡顿。参考地址
  2. 利用CADisplayLink显示屏幕刷新速率FPS;CADisplayLink是按照屏幕刷新速率来显示的,正常情况下一秒钟执行60次,当屏幕刷新速率过慢的时候就可以监测卡顿参考地址

死锁监测

通过获取线程列表信息(cpu使用率,线程状态,标识,线程名称),通过两种方式判断是否发生死锁
主线程的 CPU 占用为 0,当前处于等待的状态,已经被换出

//第一种,如果看到主线程的 CPU 占用为 0,当前处于等待的状态,已经被换出,那我们就有理由怀疑当前这次卡死可能是因为死锁导致的
if ((run_state & TH_STATE_WAITING) && (flags & TH_FLAGS_SWAPPED) && cpu_usage == 0){
    //怀疑死锁
}

获取到所有线程的状态并且筛选出所有处于等待状态的线程,再获取每个线程当前的 PC 地址,也就是正在执行的方法,并通过符号化判断它是否是一个锁等待的方法

//我们可以在卡死时获取到所有线程的状态并且筛选出所有处于等待状态的线程,再获取每个线程当前的 PC 地址,也就是正在执行的方法,并通过符号化判断它是否是一个锁等待的方法。
// 参考 SMCallStack.m   smStackOfThread
// 参考 KSStackCursor
_STRUCT_MCONTEXT machineContext;
//通过 thread_get_state 获取完整的 machineContext 信息,包含 thread 状态信息
mach_msg_type_number_t state_count = smThreadStateCountByCPU();
kern_return_t kr = thread_get_state(threads[i], smThreadStateByCPU(), (thread_state_t)&machineContext.__ss, &state_count);
if (kr != KERN_SUCCESS) {
    NSLog(@"Fail get thread: %u", threads[i]);
    continue;
}
//通过指令指针来获取当前指令地址
const uintptr_t instructionAddress = smMachInstructionPointerByCPU(&machineContext);
Dl_info info;
dladdr((void *)instructionAddress, &info);
NSLog(@"指令是啥----------%s %s",info.dli_sname,info.dli_fname);

通过__psynch_mutexwait可以发现hold_lock_thread_id(等待锁的线程id), 然后再通过线程id取得队列名称来提供辅助信息,从而排查死锁问题

if (strcmp(info.dli_sname, "__psynch_mutexwait") == 0) {
//  __psynch_mutexwait /usr/lib/system/libsystem_kernel.dylib
    //这是一个正在等待锁的线程
}

参考1:GCDFetchFeed
参考2:KSCrash

卡死崩溃监测

  1. 某次长时间的卡顿被检测到之后,记录当时所有线程的调用栈,存到数据库中作为卡死崩溃的怀疑对象。
  2. 假如在当前 runloop 的循环中进入到了下一个活跃状态,那么该卡顿不是一次卡死,就从数据库中删除该条日志。本次使用周期内,下次长时间的卡顿触发时再重新写入一条日志作为怀疑对象,依此类推
  3. 下次启动程序时检测上一次启动有没有卡死的日志(用户一次使用周期最多只会发生一次卡死),如果有,说明用户上一次使用期间最终遇到了一次长时间的卡顿,且最终 runloop 也没能进入下一个活跃状态,则标记为一次卡死崩溃上报

检测用户一次卡死的时间

  • 当监测到runloop卡顿时,如何知道是否是发生了卡死(触发卡死阈值,会触发系统的watchdog)? 触发卡死阈值之后我们可以再创建一个时间间隔比较短的定时器,每隔 1s 就检测当前 runloop 有没有进入到下一个活跃状态,如果没有,则当前的卡死时间就累加 1s,当触发到卡死阈值时,就认定发生了卡死

进程间通信

  1. URL scheme
    • app通过URL scheme跳转,实现通信
  2. Keychain
  3. UIPasteBoard
  4. UIDocumentInteractionController
    • APP之间的贡献文档,以及文档预览、打印、发邮件和复制等功能
  5. Local socket
    • 一个APP1在本地的端口port1234 进行TCP的bindlisten,另外一个APP2在同一个端口port1234发起TCP的connect连接,这样就可以建立正常的TCP连接,进行TCP通信了,然后想传什么数据就可以传什么数据了
  6. APP Groups
    • APP group用于同一个开发团队开发的APP之间,包括APP和extension之间共享同一份读写空间,进行数据共享。

JS和OC通信的方式,以及如何做到与OC的异步通信

由于JS在运行时都是单线程执行,当JS和OC在交互时采用异步设计有助于提升交互体验。JS和OC有两种通信方式:

  1. 通过拦截webview加载请求
    这种情况是使用webview发送一个url请求,通过拦截url获取并解析参数然后达到通信的方式
  2. 通过JavaScript Core
    JavaScript Core是iOS7提供的框架,目的是让OC和JS的交互变得更简单

异步通信
异步通信在第一种情况下,在JS端声明一个全局变量的数组用来保存JS通知OC的回调ID和回调方法,当每次JS调用OC的方法时都会自动生成一个回调ID并添加到全局变量的数组中,然后采用拦截url请求,OC处理完成后,主动调用js端的一个方法,这个方法就是根据回调ID取出全局对象的方法,然后执行

OC对象的内存管理

iOS当中内存存放区域分为堆区(通过alloc等分配的空间,需要开发者自己管理内存释放)、栈区(比如局部变量,不需要开发者自己管理内存释放)、代码段(编译之后的代码)、数据段(字符串常量、全局变量、静态变量等,程序退出时释放)
内存管理通过ARC(自动控制)和MRC(手动控制)来管理
当一个OC对象创建的时候引用计数就是1,调用retain方法引用计数就会+1,调用release方法引用计数就会-1,当引用计数为0时,对象就会被销毁,释放其占用的内存空间
对象的引用计数保存在isa里的extra_rc里面,另外有一个has_sidetable_rc表示引用计数过大无法存储的情况下,就会存储在一个SideTable的类的属性中
野指针
指针指向的对象被释放了,指针还在,就称为野指针,通过该指针给对象发送消息就会报错
僵尸对象
已经被收回但是这个对象的数据仍然处在内存中,像这样的对象叫做僵尸对象
空指针
空指针是指没有指向任何东西的指针(存储的东西是NULL或者是nil等)

Struct与Union主要区别

struct是结构体,union是共用体,都是由不同的数据类型成员组成,共用体在同一时刻只能存放一个成员数据(因为共用体对象共同占用一块内存),而结构体都可以存放

编译器怎么检测#import和#include导入多次的问题,三方库导入时如何设置”“和<>

#include相当于拷贝头文件中的内容,如果重复导入,会报重复定义的错误
#import在导入时,编译器会做检查,所以不会报重复定义
@class会告诉编译器XXXClassName是一个类,但具体实现细节,不需要知道
三方库通过Header Search Path设置""和<>

weak指针的原理

__unsafe_unretained修饰的变量,一旦变量被释放后,其地址还会存在,如果后续访问该变量则会报错
__weak修饰的变量,一旦变量被释放后,会自动置为nil
那么__weak是怎么实现变量自动置为nil的呢?

  1. 初始化时:runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址。
  2. 添加引用时:objc_initWeak函数会调用 objc_storeWeak() 函数, objc_storeWeak() 的作用是更新指针指向,创建对应的弱引用表。
  3. 释放时,调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。

简单来说Runtime维护了一个hash表,key是对象地址,value是weak指针的地址的数组,对象被释放时,会调用clearDeallocating函数,然后遍历这个数组把其中的数据置为nil,最后把这个entry从weak表中删除。

Block当中的weakSelf和strongSelf

weakSelf是为了block不持有self,避免Retain Circle循环引用。在Block内如果需要访问self的方法、变量,建议使用 weakSelf。

strongSelf的目的是因为一旦进入block执行,假设不允许self在这个执行过程中释放,就需要加入strongSelf。block执行完后这个strongSelf会自动释放,没有不会存在循环引用问题。如果在Block内需要多次 访问self,则需要使用strongSelf

weak和assign的区别

  1. 修饰变量类型的区别
    weak 只可以修饰对象。如果修饰基本数据类型,编译器会报错
    assign可以修饰对象和基本数据类型,在修饰对象类型时,用__unsafe_unretained
  2. 野指针上的区别
    weak不会产生野指针问题,因为weak修饰的对象在释放后,会自动置为nil,之后再向对象发送消息也不会崩溃
    assign如果修饰对象会产生野指针的问题,如果是修饰的基本数据类型则不会有问题。修饰的对象在释放后不会自动置为nil,此时向对象发送消息会产生崩溃

copy和strong的区别

经常在看到定义property时的NSString时使用copy来修饰,而不使用strong修饰,主要是有以下好处:

  • 当源字符串是NSString(不可变字符串)时,使用strong和copy来修饰,都是指向原来的对象,copy操作只是做了一层浅拷贝
  • 当源字符串是NSMutableString时,strong只是将源字符串的引用计数+1,而copy相当于源字符串做了一个拷贝,从而生成了一个新的不可变对象,源字符串发生变化时这个property并不会随着发生变化,所以我们一般不希望它跟着变化的时候使用copy来修饰

id和instencetype的区别

id可以作为方法的参数,但instancetype不可以,
ARC环境下:
instancetype用来在编译期确定实例的类型,而使用id的话,编译器不检查类型, 运行时检查类型.
MRC环境下:
instancetype和id一样,不做具体类型检查
instancetype只适用于初始化方法和便利构造器的返回值类型

Clang跟LLVM是啥区别

Clang主要是编译器进行点断语法分析、语义分析,生成中间代码, llvm是进行代码优化,生成机器码,最终执行

Runloop中Source0和Source1区别

Source1基于mach_Port的, 可以主动唤醒休眠中的RunLoop,处理来自系统内核或者其他进程或线程的事件,比如屏幕点击, 网络数据的传输都会触发sourse1
Source0处理非基于Port的事件,一般是APP内部的事件, 比如hitTest:withEvent的处理, performSelectors的事件

举个例子:
我们触摸屏幕,先摸到硬件(屏幕),屏幕表面的事件会被IOKit先包装成Event,通过mach_Port传给正在活跃的APP , Event先告诉source1(mach_port),source1唤醒RunLoop, 然后将事件Event分发给source0,然后由source0来处理。

iOS中几个常用协议

NSCopying

如果一个类要想支持copy操作,就必须要实现NSCopying协议,也就说必须实现copyWithZone方法
同理,一个类想要支持mutableCopy操作,则必须实现NSMutableCopying协议,也就是说实现mutableCopyWithZone方法
iOS系统中的一些类已经实现了NSCopying协议或者是NSMutableCopying协议,所以它们支持copy、mutableCopy方法,但如果是一个自定义的类直接调用copy或者是mutableCopy方法,则会crash

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Person copyWithZone:]: unrecognized selector sent to instance 0x6080000314c0'

此时就需要实现以下协议:

- (id)copyWithZone:(NSZone *)zone {    
    Person *model = [[[self class] allocWithZone:zone] init];
    model.firstName = self.firstName;
    model.lastName  = self.lastName;
    //未公开的成员
    model->_nickName = _nickName;
    return model;
}

NSCoding

如果我们要将应用数据存储到硬盘中,没有实现NSCoding协议就会报错,NSCoding是一个你需要在数据类上要实现的协议以支持数据类和数据流间的编码和解码。数据流可以持久化到硬盘。

@implementation People 
- (void)encodeWithCoder:(NSCoder *)aCoder
{
    [aCoder encodeObject:self.name forKey:NSStringFromSelector(@selector(name))];
    [aCoder encodeObject:self.age forKey:NSStringFromSelector(@selector(age))];
}

- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder
{
    if (self = [super init]) {
        self.name = [aDecoder decodeObjectForKey:NSStringFromSelector(@selector(name))];
        self.age = [aDecoder decodeObjectForKey:NSStringFromSelector(@selector(age))];
    }
    return self;
}
@end

只要实现这两个方法,就可以对所有对象的属性进行编码和解码,然后便可以对对象进行归档,并且可以将其写入归档或者从归档中读取它们。

关于布局上的几个API

  • - (BOOL)needsUpdateConstraints
    • 使用此返回值去决定是否需要 调用updateConstraints作为正常布局过程的一部分
  • - (void)setNeedsUpdateConstraints
    • 当一个自定义view的某个属性发生改变,并且可能影响到constraint时,需要调用此方法去标记constraints需要在未来的某个点更新,系统然后调用updateConstraints.
  • - (void)updateConstraints
    • 更新约束,自定义view应该重写此方法在其中建立constraints. 注意:要在实现在最后调用[super updateConstraints]
  • - (void)updateConstraintsIfNeeded
    • 立即触发约束更新,自动更新布局。
  • - (void)layoutSubviews
    • 需要更精确控制子view,而不是使用限制或autoresizing行为,就需要实现该方法
  • - (void)layoutIfNeeded
    • 此方法强制立即进行layout,从当前view开始,此方法会遍历整个view层次(包括superviews)请求layout。因此,调用此方法会强制整个view层次布局。
  • - (void)setNeedsLayout
    • 此方法会将view当前的layout设置为无效的,并在下一个upadte cycle里去触发layout更新
  • - (void)didAddSubview:(UIView *)subview
    • 通知视图指定子视图已经添加
  • - (void)willRemoveSubview:(UIView *)subview
    • 通知视图将要移除指定的子视图
  • - (void)willMoveToSuperview:(UIView *)newSuperview
    • 通知视图将要移动到一个新的父视图中
  • - (void)didMoveToSuperview
    • 通知视图已经移动到一个新的父视图中
  • - (void)sizeToFit
    • 计算出最适合子视图的大小,也就是说自动适应子视图的大小(这个方法会更改子视图的宽高)
  • - (CGSize)sizeThatFits:(CGSize)size
    • [label sizeThatFits:CGSizeZero]返回最适合子视图的大小(这个方法不会更改子视图的宽高,但会返回)

在一个ViewController中,如果想要更新视图的约束,可以通过重写这个方法去更新当前View的内部布局,这个方法默认的实现是调用对应View的 -updateConstraints。我们在重写这个方法时,务必要调用 super 或者 调用当前View的 updateConstraints 方法

- (void)updateViewConstraints
{
    // 在这里为你的view添加约束,请确保该view的translatesAutoresizingMaskIntoConstraints属性已设置为NO
    [super updateViewConstraints];
}

子线程刷新UI

UIKit框架不是线程安全的, 多个线程同时操作UI,那么必然会导致视图的错乱等等,试想一下,一个线程remove视图,另外一个线程获取这个视图,会发生什么情况?
我们看到的子线程能够更新UI的原因是,等到子线程执行完毕,自动进入了主线程去执行子线程中更新UI的代码, 可以在子线程sleep几秒做个实验
对于子线程刷新UI,如果说想要统一通过外部手段进行防护,通常采用runtime交换几个刷新UI的方法:

[NSObject wbg_swizzlingClass:[self class] orginalMethod:@selector(setNeedsLayout) customMethod:@selector(wbg_sf_layer_setNeedsLayout)];

[NSObject wbg_swizzlingClass:[self class] orginalMethod:@selector(setNeedsDisplay) customMethod:@selector(wbg_sf_layer_setNeedsDisplay)];

[NSObject wbg_swizzlingClass:[self class] orginalMethod:@selector(setNeedsDisplayInRect) customMethod:@selector(wbg_sf_layer_setNeedsDisplayInRect:)];

NSURLConnection和NSURLSession的区别

执行方式

  1. NSURLConnection创建后就会执行,cancel方法可以停止请求的发送,但停止后不能继续执行,需要重新创建新的请求。
  2. NSURLSession创建后,task是挂起状态,需要resume才能执行;有三个控制方法:取消(cancel)、暂停(suspend)、继续(resume),暂停以后可以通过继续恢复当前的请求任务。

下载任务的方式

  1. NSURLConnection下载文件时,先是将文件下载到内存中,然后在写入到沙盒,如果文件比较大,会出现内存暴涨的情况
  2. NSURLSessionDownloadTask是默认将文件下载到沙盒中,不会出现内存暴涨的情况 断点续传的方式
  3. NSURLConnection断点续传功能,依赖于请求的HTTPHeaderField的Range属性,接收到下载数据时代理方法就会持续调用,使用NSOutputStream管道流进行数据保存
  4. NSURLSession进行断点下载功能,当暂停下载任务后,如果downloadTask非空,调用cancelByProducingResumeData:(void (^)(NSData *resumeData))completionHandler这个方法,这个方法接收一个参数,完成处理代码块,这个代码块有一个NSData参数resumeData,如果resumeData非空,我们就保存这个对象到视图控制器的resumeData属性中,在点击再次下载时,通过调用[[self.session downloadTaskWithResumeData:self.resumeData]resume]方法进行继续下载操作

配置信息

  1. NSURLConnection依赖一个全局的配置,缺乏灵活性
  2. NSURLSession构造方法sessionWithConfiguration:delegate:delegateQueue中NSURLSessionConfiguration可以设置配置信息:cookie、缓存策略、最大连接数、资源管理、网络超时等配置

HTTPS加密过程

HTTPS加密其本质上就是利用了非对称加密对称加密两种手段来确保数据传输的安全性, 非对称加密来确保密钥交换的安全性,对称加密来确保数据传输的快捷性

  1. 客户端访问https网址
  2. 服务器明文返回公钥,客户端验证证书是否有效、合法(浏览器行为)
  3. 客户端生成随机数,再根据公钥证书1生成一个密钥(这个密钥后续用来加密和解密请求信息),然后回传给服务器,服务器用私钥对该信息解密,得到这个密钥,这样客户端和服务器都具有密钥
  4. 客户端和服务器之间使用密钥对传输信息进行加密请求,这样即使第三方抓包,也无法轻易获取通信内容,因为这个密钥只有客户端服务器知道

Category添加property

Category添加属性是可以编译通过的,而添加成员变量是无法编译的。分类没有自己的isa指针. 类最开始生成了很多基本属性,比如IvarList,MethodList。分类只会将自己的method attach到主类,并不会影响到主类的IvarList.
如果需要实现添加成员变量,可以通过objc_setAssociatedObject来实现属性的gettersetter方法

@synthesize和@dynamic分别有什么作用

@synthesize 的语义是如果你没有手动实现 setter 方法和 getter 方法,那么编译器会自动为你加上这两个方法, 默认编译器就会帮我们加上@syntheszie var = _var
@dynamic 告诉编译器:属性的 settergetter 方法由用户自己实现,不自动生成

假如一个属性被声明为 @dynamic var,然后你没有提供 @setter方法和 @getter 方法,编译的时候没问题,但是当程序运行到 instance.var = someVar,由于缺 setter 方法会导致程序崩溃;或者当运行到 someVar = var 时,由于缺 getter 方法同样会导致崩溃。编译时没问题,运行时才执行相应的方法

delegate属性为什么要用weak修饰?

控制器强引用持有UITableView, UITableViewdelegate属性, 如果delegate属性使用的strong,就造成了循环引用,使用weak就不会造成这个问题
控制器 ->UITableView -> delegate -> 控制器

delegate和dataSource有什么区别?
delegate偏向于处理用户交互,而dataSource偏向于进行数据的回调

block和delegate的区别
block其实是一个对象, 而delegate是一种设计模式
block是一种轻量级的回调,可以直接访问上下文, block容易造成循环引用,而delegate不会

iOS的自动化测试

  • XCTest
    • XCTest 是 Apple 的官方测试框架,用于执行单元测试
  • XCUITest
    • XCUITest 是一个 UI 测试框架,它构建在 XCTest 框架之上,并包括辅助 UI 测试的补充类
  • 其他第三方开源的比如Appium
    • Appium是一个开源的、跨平台的自动化测试工具,支持IOS、Android和FirefoxOS平台。 通过Appium,开发者无需重新编译app或者做任何调整,就可以测试移动应用,可以使测试代码访问后端API和数据库。它是通过驱动苹果的UIAutomation和Android的UiAutomator框架来实现的双平台支持,同时绑定了Selenium WebDriver用于老的Android平台测试。开发者可以使用WebDriver兼容的任何语言编写测试脚本,如Ruby,C#,Java, JS,OC, PHP,Python,Perl和Clojure 语言

NSZombie对象定位与监控

XCode支持开启僵尸对象监控(EditScheme->Run->Diagnostics),但只能在Debug阶段实行,其原理是HookNSObjectdealloc方法,通过调用自己的__dealloc_zombie方法把对象进行僵尸化,当访问这个已经被释放的对象时候,会转入到这个僵尸对象上面,并打印log
模仿Apple自己实现僵尸对象的监测

用于实现消息转发的类:

// _YHZombie_.h
#import <Foundation/Foundation.h>

@interface _YHZombie_ : NSObject

@end


//  _YHZombie_.m
#import "_YHZombie_.h"

@implementation _YHZombie_

// 调用这个对象对的所有方法都hook住进行LOG
- (id)forwardingTargetForSelector:(SEL)aSelector {
    NSLog(@"%p-[%@ %@]:%@",self ,[NSStringFromClass(self.class) componentsSeparatedByString:@"_YHZombie_"].lastObject, NSStringFromSelector(aSelector), @"向已经dealloc的对象发送了消息");
    // 结束当前线程
    abort();
}

@end

用于替换NSObjectdealloc方法:

//  NSObject+YHZombiesNSObject.h
#import <Foundation/Foundation.h>

@interface NSObject (YHZombiesNSObject)

@end


//  NSObject+YHZombiesNSObject.m
#import "NSObject+YHZombiesNSObject.h"
#import <objc/objc.h>
#import <objc/runtime.h>

@implementation NSObject (YHZombiesNSObject)

+(void)load {
    [self __YHZobiesObject];
}

+ (void)__YHZobiesObject {
    char *clsChars = "NSObject";
    Class cls = objc_lookUpClass(clsChars);
    Method oriMethod = class_getInstanceMethod(cls, NSSelectorFromString(@"dealloc"));
    Method newMethod = class_getInstanceMethod(cls, NSSelectorFromString(@"__YHDealloc_zombie"));
    method_exchangeImplementations(oriMethod, newMethod);
}

- (void)__YHDealloc_zombie {
    const char *className = object_getClassName(self);
    char *zombieClassName = NULL;
    asprintf(&zombieClassName, "_YHZombie_%s", className);
    Class zombieClass = objc_getClass(zombieClassName);
    if (zombieClass == Nil) {
        zombieClass = objc_duplicateClass(objc_getClass("_YHZombie_"), zombieClassName, 0);
    }
    objc_destructInstance(self);
    object_setClass(self, zombieClass);
    if (zombieClassName != NULL)
    {
        free(zombieClassName);
    }
}
@end

swift访问控制级别

访问权限open > public > internal > fileprivate > private

  • open
    • 具备最高访问权限,其修饰的类可以和方法,可以在任意 模块中被访问和重写
  • public
    • 权限仅次于 open,和 open 唯一的区别是: 不允许其他模块进行继承、重写
  • internal
    • 默认权限, 只允许在当前的模块中访问,可以继承和重写,不允许在其他模块中访问
  • fileprivate
    • 修饰的对象只允许在当前的文件中访问
  • private
    • 最低级别访问权限,只允许在定义的作用域内访问

swift 与oc的区别

  • swift更加安全,它是类型安全的语言
  • swift协议增强
    • 协议是可多继承和扩展的,还可以有计算属性
    • 协议遵循不仅只有类,包括结构体、枚举都可以遵循协议
    • 协议支持泛型
  • swift枚举增强
    • OC中枚举只能是int类型,swift则不是,除了int还可以是字符串等类型
    • 枚举的变量,还可以增加参数
    • swift的枚举可以声明变量方法
    • 枚举可以遵循协议
  • swift中的函数更灵活
    • 支持函数嵌套
    • 支持多返回值(元组数据类型)
    • 支持参数的默认值
  • swift对于泛型的支持更加好
  • swift对于访问权限控制更多
    • swift有open、public、internal、fileprivate、private权限
    • oc对于私有属性通常会放在匿名扩展中

源码阅读JavaScriptBridge

  1. OC在JavaScriptBridge环境初始化时,会向web端注入一段js,js在初始化时会向webView注入一个不可见的iframe,其src地址为https://wvjb_queue_message
  2. 当JS调用OC方法,如果有callback时,则会生成一个callbackId(callbackId生成规则是cb+全局变量自增ID+时间戳),并将这个callbackId和callback保存到全局变量数组里面,同时也会将message发送的数据保存到全局对象sendMessageQueue,然后调用iframe的src地址
  3. 接下来webview会拦截这个请求,通过scheme和host判断是否是需要OC处理的交互方法,然后通过stringByEvaluatingJavaScriptFromString方法执行js方法_fetchQueue,这里面会将sendMessageQueue取出来返回,OC拿到sendMessageQueue取出来的message数据,然后在将其序列化为数组,依次遍历取出handleName并将其转换为WVJBHandler(在OC注册方法时有一个可变字典来保存注册的handleName和方法,这里是根据handleName获取到这个可变字典的存储的方法),然后调用。
  4. 如果有回调的话,OC会将js调用OC方法传入的这个message数据的callbackID和responseData在回传给js端,最终会通过闭包调用_doDispatchMessageFromObjC方法,回调给js端数据,然后会删除全局对象里保存的callbackId。 因为单次调用后,callbackId就会被删除(为了避免js内存占用过大),这也就造成了每次事件响应必须得和OC通信。
  5. 比如在JS端通过一个方法创建了一个button(方法里面有一个callback用来标记button按钮点击的时候通知到JS端),这也就造成了创建完成后事件只能在web端响应一次的后果。

源码阅读FDTemplateLayoutCell

FDTemplateLayoutCell采用了category的形式编写,其好处是方便调用

  1. 一开始在tableView执行heightForRowAtIndexPath:方法时,会懒加载cell,并不会被显示在屏幕上
  2. 然后使用-systemLayoutSizeFittingSize:方法计算AutoLayout对象所占用的大小
  3. 计算好的高度大小会放到一个二维数组当中(有两个二维数组,一个存放横屏状态下cell的大小,一个存放竖屏状态下的大小),滑动tableview获取高度时会优先从数组当中获取
  4. 这个库在+load方法中hook了tableView的reloadData/insert/delete等方法,然后会对缓存高度的二维数组处理

源码阅读YYImage

图片加载的性能优化中已经说到过加载UIImage的一些影响性能的问题,而YYImage正是解决了这些性能问题

YYImage中的几个核心类

  1. YYImage:UIImage的子类,遵守 YYAnimatedImage 协议,帧图片,编解码,帧预加载等高级特性,支持WebP,APNG和GIF的编解码
  2. YYFrameImage:UIImage的子类,能够显示帧动画,仅支持png,jpeg 格式
  3. YYSpriteSheetImage:是用来做Spritesheet动画显示的图像类,也是UIImage的子类,可以理解为一张大图上分布有很多完整的小图,然后不同时刻显示不同位置的小图。
  4. YYImageCoder : 图像的编码和解码功能类,YYImage底层支持,YYImageEncoder负责编码,YYImageDecoder 负责解码,YYImageFrame 负责管理帧图像信息,_YYImageDecoderFrame 内部私有类是其子类,UIImage+YYImageCoder提供了一些便利方法
  5. YYAnimatedImageView: UIImageView 子类,用于播放图像动画

YYImage

YYImage的属性定义:

@interface YYImage : UIImage <YYAnimatedImage>
// 以下方式加载都会实现自定义解码,并且不会对缓存的结果进行缓存
+ (nullable YYImage *)imageNamed:(NSString *)name;
+ (nullable YYImage *)imageWithContentsOfFile:(NSString *)path;
+ (nullable YYImage *)imageWithData:(NSData *)data;
+ (nullable YYImage *)imageWithData:(NSData *)data scale:(CGFloat)scale;

@property (nonatomic, readonly) YYImageType animatedImageType; // 图像类型
@property (nullable, nonatomic, readonly) NSData *animatedImageData; // 动态图像的元数据
@property (nonatomic, readonly) NSUInteger animatedImageMemorySize; // 多帧图像内存占用量
@property (nonatomic) BOOL preloadAllAnimatedImageFrames; // 预加载所有帧(到内存)

@end

如果调用preloadAllAnimatedImageFrames setter方法开启预加载所有帧,则会 进入:

- (void)setPreloadAllAnimatedImageFrames:(BOOL)preloadAllAnimatedImageFrames {
    if (_preloadAllAnimatedImageFrames != preloadAllAnimatedImageFrames) {
        if (preloadAllAnimatedImageFrames && _decoder.frameCount > 0) {
            NSMutableArray *frames = [NSMutableArray new];
            //拿到所有帧的图片
            for (NSUInteger i = 0, max = _decoder.frameCount; i < max; i++) {
                UIImage *img = [self animatedImageFrameAtIndex:i];
                [frames addObject:img ?: [NSNull null]];
            }
            dispatch_semaphore_wait(_preloadedLock, DISPATCH_TIME_FOREVER);
            _preloadedFrames = frames; // 将所有帧数据赋值给私有变量_preloadedFrames保存
            dispatch_semaphore_signal(_preloadedLock);
        } else {
            dispatch_semaphore_wait(_preloadedLock, DISPATCH_TIME_FOREVER);
            _preloadedFrames = nil;
            dispatch_semaphore_signal(_preloadedLock);
        }
    }
}

YYImage的私有变量:

@implementation YYImage {
    YYImageDecoder *_decoder; // 图像解码器
    NSArray *_preloadedFrames; // 预加载的图像帧
    dispatch_semaphore_t _preloadedLock; // 预加载锁,用来保证_preloadedFrames的读写
    NSUInteger _bytesPerFrame; // 内存占用量
}
  1. YYImage 继承自UIImage,并重写了imageNamed:、imageWithContentsOfFile:、imageWithData:以禁用缓存
  2. 调用上述方法时,最终会来到initWithData:scale方法,进入到方法后初始化了信号量 (作为锁)、图片解码器 (YYImageDecoder),以及通过解码器获取第一帧解压过后的图像等。最终调用initWithCGImage:scale:orientation获取Image实例。 参考地址1    参考地址2