iOS底层原理学习笔记

  1. 怎样将oc代码反编译成C和C++代码?
    使用xcode内置的LLVM的前端编译器clang,这样生成的代码并不完全是底层实现,只是一个参考
    命令:clang -rewrite-objc 文件名称.m -o 输出文件名称.cpp
    指定平台命令:xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc oc源文件.m -o 目标输出文件.cpp
    如果需要连接其他框架,-framework 框架名(UIKit)
    添加arc支持 -fobjc-arc -fobjc-runtime=ios-8.0
  2. 获取oc源码
    从官网下载,地址:https://opensource.apple.com/tarballs/objc4
  3. 在xcode实时查看内存数据
    Debug -> Debug workflow -> View Memory
    也可以使用LLDB指令,也就是xcode的控制台
  4. 常用的LLDB指令
    1. p <====> 打印
    2. po <===> 打印对象
    3. memory read/数据格式字节数 内存地址 <===> 读取内存
    4. x/数量(几段) 格式(进制数 x=16进制 f=浮点 d=10进制) 字节数(b=1 h=2 w=4 g=8) 内存地址 <===> 读取内存
    5. memory write 内存地址 数值 <===> 修改内存中的值
    6. step 单步执行OC代码
    7. stepi == si 单步执行汇编代码
    8. continue 继续执行,跳到下一个断点
    9. next 单个函数执行
  5. 在控制台通过函数体地址获取函数信息
    p (IMP)地址
  6. LLVM中间代码的生成
    OC代码经过LLVM经过编译之后会先生成跨平台的中间代码,然后再生成汇编代码及二进制
    生成中间的代码的命令是:clang -emit-llvm -S 文件名
    这个中间代码是一种LLVM独有的语言,官方文档:https://llvm.org/docs/LangRef.html
  7. 在调试过程中控制台查看所有的调用栈
    控制台输入命令:bt
  8. iOS Fundation框架源码
    GUNStep计划将OC的库从新实现一遍并且进行开源,源码接近于苹果的源码
    源码地址:https://www.gunstep.org/resources/downloads.php
  9. GCD源码
    https://github.com/apple/swift-corelibs-libdispatach
  1. OC对象的本质

    • OC中类和对象都是基于C和C++的结构体实现的;
    • OC中的对象分为三种:实例对象、类对象、元类对象
    实例对象的内存分配情况
    • OC中的基类就是NSObject,这个纯洁的实例对象实际上只有一个成员变量isa,isa是一个指针,在64位系统下占用8个字节。然而NSObject在实例化后他所分配和占用的空间是16字节,在源码中有注释说明一个oc对象的最小占用16个字节;
      其实OC在对象的内存分配和占用上做了优化,学名叫'字节对齐'。在占用内存上它规定结构体的大小必须是最大成员大小的倍数。在分配内存上至少是16或者16的倍数
      CPU读取内存的时候分为大端和小端模式,iOS是使用的小端模式,也就是从高位开始读
    • 实例对象就是通过alloc分配内存生成的对象,内部结构是一个isa指针和它的成员变量的值;
    • 类对象在内存中有且只有一个,通过类和实例都能获取到类对象。其中包括isa指针,superClass指针,类的属性信息,类的对象方法信息,类的协议信息、类的成员变量信息;
    • 元类对象在每个类的内存中也只有一个,通过runtime方法object_getClass(类对象)获取。内部结构跟类对象一样都是Class类型,只是用途不一样。其中包括:isa指针,superclass指针,类的类方法信息;
  2. isa指针和superclass指针

image.png
  • 实例对象的isa指向类对象,类对象的isa指向元类对象,元类对象的isa指向基元类,基元类的isa则指向自己;
  • 实例对象中没有superclass指针,只有类对象和元类对象中有,superclass就是指向父类,根元类对象的superclass指向根类对象,根类对象的superclass指向nil
  • 在64位架构以前实例对象的isa地址值直接就是类对象的地址值,在arm64架构之后需要用实例对象的isa的地址值 & ISA_MASK(arm64 == 0x0000000ffffffff8 x_86_64 == 0x00007fffffffff8)才能获取到真正的类对象的地址值。 类对象同样如此;
  • 目前OC最新的版本是2.0,在源码中的实现也进行了更改,其中最大的变化是对象的结构体实现使用的是c++的方式实现的(其中FASK_DATA_MASK == 0x00007ffffffffff8);
  • 如果想要窥探到类对象中的结构,需要仿照源码写一套同样的结构的结构体出来(类实际上就是结构体),然后对类对象进行强转;


    截屏2021-07-25 上午10.09.40.png
  1. KVO

  • Key Value Observing 健值监听技术,可以对任意对象的属性进行监听;
  • 当一个对象A的属性被监听的后,程序运行中会动态生成一个NSKVONotifying_A并且继承自A类的子类 。同时对象A的isa指针会指向这个子类NSKVONotifying_A。在调用A对象属性的set方法时,会通过isa指针找到类对象NSKVONotifying_A的set方法。在该set方法中时实现大概是先调用willChangeValue 方法,再调用super的set方法,最后再调用didChangeValue方法并且调用observer的observerValueForKeypath方法实现监听;
  • 在NSKVONotifying_A类中还实现了class、dealloc、isKVOA方法;
  • 如果想要手动触发kvo,可以先调用对象的willChangeValueForKey方法,再调用didChangeValueForkey方法;
  1. KVC

  • Key Value Coding 健值编码技术,通过key获取或设置对象的属性的值;

  • setValue:forKey:调用流程;
    假如key == name

    1. 先去查找setName的方法实现进行赋值;
    2. 如果没有找到setName方法的实现,就会调用对象的accessInstanceVariablesDirectly方法,查询是否能够直接访问方法变量;
      • 如果是No,直接抛出异常。
      • 如果是Yes,会依次对name, isName, _name, _isName的变量进行赋值,如果这个几个变量都没有也会抛出异常;
        如果赋值成功,会触发KVO
  • getValueForKey:调用流程;

    1. 先调用getName方法获取值;
    2. 如果没有getName的方法实现,就会访问对象的accessInstanceVariablesDirectly方法确认是否能直接访问变量;
      • 如果是NO,直接抛出异常。
      • 如果是YES,依次访问成员变量_name, _isName, name, isName的值
  1. Category

    • 所有的分类在程序编译时,会被包装成category_t结构体,里面存放着,类名,class指针,对象方法和类方法列表,属性列表,协议列表;

    • 在程序运行的时候,会将所有分类的数据合并到类对象中去。比如分类中的方法列表;

      1. 倒序遍历分类数组,取出分类中的方法列表。
      2. 类对象中的方法列表是一个二维数组进行维护的。将二维数组扩容,再将类对象中的方法列表移到数组最后,在所有分类中的方法列表生成的二维数组,插到类对象的方法二维数组前面。
    • 所以如果分类中重写了类对象的方法,会优先调用分类中的方法;

    • 在程序启动的时候,不管类或者分类有没有被使用,都会加载compile sources列表中类和分类的load方法。调用顺序会根据xcode中compile sources列表顺序优先调用所有类的load,并且先调用父类。再根据compile sources列表顺序加载调用所有的分类的load。在调用load方法的时候,使用的函数地址直接调用,并没有使用消息发送机制,所以每个类和分类的load都会调用

    • initialize方法只有在类第一次接收消息的时候只会调用一次,也就是使用objc_messageSend方式,并且会优先调用父类的initialize方法。如果子类中没有实现initailize,就会调用父类的initialize,因为走得是消息发送机制,所以在这种情况下,父类的initialize会被调用多次。

      关联对象
    • 为分类属性提供储存属性值的地方,通过<objc/runtime.h>api的objc_setAssociatedObject和objc_getAssociatedObject保存和获取属性值。 其中的key最好的方案是使用该属性的get方法,易懂,方便,唯一;
      _cmd 表示当前方法的selector

    • 关联对象实现原理:它是由AssociationsManager、AssociationsHasMap、ObjectAssociationMap、ObjectAssociation协作完成的。跟原来的类对象不发生关系。如果给A对象分类的属性name赋值,流程如下:

      1. 通过AssociationsManager拿到AssociationsHasMap;
      2. 通过对对象A的地址哈希后的值从AssociationsHasMap里面找出ObjectAssociationMap;
      3. 通过objc_setAssociatedObjec方法中传入的key,在ObjectAssociationMap中找出ObjectAssociation;
      4. 最后将objc_setAssociatedObjec传入的value和policy存入ObjectAssociation;


        image.png
  2. Block

    • Block本质上也是一个oc对象,主要封装了函数调用及函数调用的环境。主要信息有:isa指针,函数地址,block的描述如Block的size等,捕获的外部的auto局部变量;

    • 在引用外部变量的时候不同的情况有不同的捕获机制,只要是auto局部变量就会被捕获;
      auto 修饰的局部变量是指会自动销毁的变量。C语言中定义的局部变量默认就是auto修饰的。
      在oc函数中都会隐式的传入self和_cmd两个参数。所以如果block中引用了self或者是self的变量,也会被当做局部变量进行捕获。

      image.png

      • block有三种类型,分别是:
        1. 全局block(NSGlobalBlock存放在数据区);
        2. 堆block(NSMallocBlock存放在堆区,程序员自己管理);
        3. 栈block(NSStackBlock存放在栈区,自动销毁);
          他们最终都是继承自NSBlock,block中的isa就指向这些类对象。
      block类型 生成条件 copy操作
      NSGlobalBlock 内部没有访问auto变量 什么也不做依然是global类型
      NSStackBlock 内部访问了auto变量在arc下依然会变成malloc类型 变成NSMallocBlock
      NSMallocBlock NSStackBlock 调用copy 引用计数增加
      • 在ARC环境时,block在某些特定的环境下,栈区block会自动进行copy操作变成堆区block。如;
        1. block作为函数返回值时
        2. 使用强指针引用时
        3. 使用usingBlock时,比如数组的排序方法
        4. GCD的block
      • 栈区block在内部引用了auto的对象变量时,不管该对象是strong修饰还是weak修饰都不会进行强引用。但是当栈区block从栈区拷贝到堆区时,在block内部会自动根据对象的修饰符进行copy操作,如果是strong被引用对象的引用计数+1,如果是weak,什么都不做。当堆区block在销毁也会被引用auto对象变量进行dispose操作,被引用对象引用计数-1。
      • __block可以用来修饰auto变量,以达到可以在block内部修改变量的值。
      • 假如__block修饰的auto变量A,在block内部会被包装成一个对象(暂且叫block_A),这个对象内部有:isa、forwarding指针、size、变量A,如果A是个对象时,还会多一个copy和dispose的函数指针(跟外面那层的copy和dispose功能类似)。其中forwarding指针是指向它自己的,block_a中的A就是外面__block修饰的变量A。 当这个栈block拷贝到堆区的时候,block_a中的forwarding指针就会指向堆区block的block_a,这样就能保证不管在栈区还是堆区,访问的对象都是同一个变量A。
      • 在MRC环境下,block内部对__block修饰的对象类型不会自动进行强引用。
      • 解决由于block引用外部对象变量时,产生的循环引用,造成内存泄漏的问题:
        1. 使用__weak修饰外部对象变量。__weak还有一个好处——weak引用的对象销毁后,指针会设置为nil;
        2. 使用__unsafe__unretained修饰外部对象变量。这种方案可能导致野指针错误,因为对象销毁后指针还是会指向对象所在的地址;
        3. 使用__block修饰外部对象变量,并且在block内部将对象设置为nil,而且还必须调用这个block,才会释放所有对象。但是在MRC环境下就是跟__unsafe__retained的效果一样了
      • 说明一下__weak修饰了外部对象变量以后,为什么需要再block内部再对对象变量__strong重新去修饰一下,之前我也是不太理解。首先,__strong是为了保证在整个block内部生命周期内,对象变量不会被销毁;再者,__weak修饰的对象变量如果直接访问成员变量,编译器也会报错,因为对象可能随时会销毁。那调用方法为什么不报错呢?是因为OC支持nil可以调用任何方法。
  3. Runtime

    • Object-C是一门动态性很强的语言,内部就是基于runtime实现了动态性支持;
    isa指针
    • 在arm64架构之前,isa就是一个普通的指针,直接存储着类对象和元类的地址。在arm64之后,isa被优化了,使用位域存储了更多的信息,成了一个union共用体。
      image.png

      - union是共用体,顾名思义,就是内存共用的意思。其中struct没有实际作用,只是相当于一个隐式注释,表明在bits中存储这些东西并且注明了占用的位数。 位域就是表示占用了多少位。
      1. nonpointer——地址中是否包含了其他信息
      2. has_assoc——是否包含了关联对象
      3. has_cxx_dtor——是否有c++的析构函数
      4. shiftclas——class指针地址
      5. magic——在调试时,对象是否未完成初始化
      6. weakly_referenced——是否有被弱引用指向过
      7. deallocating——是否正在释放
      8. has_sidetable_rc——当extra_rc存放的引用计数放不下时,是否将引用计数放到sidetable中
      9. extra_rc——存放对象的引用计数数值
回顾一下位运算中的与、或、反码,左移的用法;
1. 与  符号 = & 两位同时为“1”,结果才为“1”,否则为0
#假如想取出一个二进制数中的第3位数值
int a = 1;
#如果result > 0 就代表第三位是1
int result = a & (1 << 3)
2. 或   符号 = | 两位只要有一个为1,其值为1,否则为0
#如果想改变某一位的值 就用或
a = a | (1 << 3);
3. 反码 符号 = ~ 取相反的值 如果0b0100  结果:0b1011
4. 左移 符号 = <<  将数值以二进制的方式向左移,如0001<<4,结果:1000 
  • 类对象中的属性协议等信息是由class_rw_t管理的,但是在类初始化的时候会将类的初始化信息放到class_ro_t中保存的,并且其中存放的属性方法协议等信息都是只读的。类信息初始化完之后再将class_ro_t交给class_rw_t管理。不同的是class_rw_t的是信息是可以修改的。

  • method_t是函数咋runtime的封装,里面包含了函数名SEL、返回值类型和方法类型types、函数体指针IMP。
    SEL其实是跟char*差不多的东西,可以通过Selector和sel_registerName获取;
    types存放了函数返回值和参数的类型编码字符串。可以使用@encode查看具体类型的具体编码。

    char *types = ‘i24@0:8i16f20’
    1. 第一个i 表示返回值是一个int类型, 占用4个字节;
    2. 24 表示函数的参数总大小;
    3. @ 表示id类型,占用8个字节,每个函数都会默认传入两个参数slef和_cmd;
    4. 0 表示self是从0位开始;
    5. : 表示方法_cmd,占用8个字节;
    6. 8 代表_cmd从第8位开始;
    7. i 表示int类型形参,占用4个字节;
    8. f 表示float类型的形参,占用4个字节;
    
  • 在类对象内部也维护了一个方法缓存列表cache_t,当对象第一次调用一个方法的时候,为了提高效率,该方法会被缓存到这个列表中,实际上维护的是一个散列表。


    image.png
  • 那么cache_t是怎样做到方法缓存优化的呢?
    其核心就是维护了一张散列表(默认散列表的长度是4),从算法的角度来讲就是以空间换时间。当缓存一个名为test的方法时,先使用test & _mask得出散列表的下标,这个下标的值必定是小于_mask的值,然后在将方法名和函数体指针封装成bucket_t根据下标放到散列表_buckets中。
    值得注意的是:方法名 & _mask得到的下标值index会有重复的情况,这时候发现散列表中的下标位置是有值的那么就会index会进行减1操作直到找到空位置存储。(当然也会判断下标位置中的key是否一致,如果一致直接覆盖)
    还有存满的情况,也就是上面那种情况,直到下标值到0 还是没有空位置,就从最大的下标继续查找直到begain的位置。当散列表存满的时候,就对散列表扩容至原基础上的两倍大,再清除缓存,重新存储。

  • 当一个对象调用一个方法的时候,最终会转成objc_messageSend给class发送消息,如果最后查到superclass为nil,就会进入动态方法解析流程,如果动态解析还没处理就会进入消息转发流程;

  1. 消息发送流程如下图:
大致主要流程是:
1. 通过对像的isa找到类对象或元类对象;
2. 从类对象中的缓存cache_t查找,这里假装没有;
3. 从类对象中class_rw_t的方法列表中查找,这里又假装没有;(如果查到了,会存到对象的cache_t里面去,流程结束)
4. 从父类中查找,直到superclass为nil,进入动态方法解析流程;(如果父类找到了,回到第3步)
image.png
  1. 动态方法解析流程:
    动态方法解析流程只会进入一次
1. 不管对象有没有实现类方法resolveInstanceMethod或resolveClassMethod,都会尝试调用并再一次进入消息发送流程。这里假装实现了;
2. 在方法内部可以通过class_addMethod为该方法动态添加一个实现,这里假装没有实现;
3. 进入消息转发流程(动态添加实现了 没有这一步了);
image.png
  1. 消息转发流程:
    如果是类方法,以下几种涉及的方法就改成类方法
1. 调用forwardingTargetForSelector:方法,
返回能处理这个方法的对象。这里假装返回nil;
2. 调用methodSignatureForSelector:方法,
返回方法签名(即返回值、参数类型编码)。这里假装返回了;
3. 调用forwardingInovacation:方法,
invocation中包含了方法接收者、方法名、方法签名。执行invoke就会重新发送消息。其实到了这一步就标志着流程结束了;
image.png
  • super的理解:使用super调用方法,意思就是从父类开始查找这个方法并调用,但实际接收者还是self。super最终会转成objc_messageSendSuper方法发送消息,参数只是比bjc_messageSend多传了一个父类。
  1. Runloop

  • Runloop就是保证程序不被退出,并且在运行过程中持续做一些事情。iOS程序中的触摸事件、定时器、GCD、网络等事件都依赖于runloop。runloop能够做到有事情就马上处理,没事情就挂起,不消耗cpu,性能高的特点;

  • Runloop与线程是一一对应的,每一个线程都有一个runloop,但是runloop并不是自动创建的,只有在获取的时候才会去创建runloop。在iOS中主线程的runloop是自动创建的。在程序中有一个全局的字典以线程为key保存着所有的runloop。runloop也会随着线程销毁而消失;

  • runloop可以存在多个运行模式model,但是一次只会运行一个模式,在mode进行切换的时候,只能先退出(这里的退出并不是循环),再重新进入。每个model中包含了多个source0和source1(触摸事件等)、timer(定时器事件)、observer(监听事件),没有这几个都是空的runloop会立即退出;


    image.png
    1. source0:处理触摸事件处理和performSelector:onThread:
    2. source1: 处理基于port的线程间通信 和系统事件的捕捉(屏幕触摸事件先是经过source1包装再交给source0处理的)
    3. Timers: 处理NSTimer 和performSelector:withObject:afterDelay:
    4. Observers: 监听runloop的状态,处理UI刷新和AutoreleasePool,都是在runloop即将进入睡眠之前操作的。
  • 常用的runloop模式有default和tricking,前者是默认的模式,后者则是scrollview滚动时的model。commonModels则是包含了这两种模式;

    • runloop运行逻辑


      image.png
  • runloop的休眠实现原理,runloop的休眠能够真正做到cpu不做任何事情。这里主要涉及到状态的切换,需要休眠就会切换到内核态,如果有消息要要处理就从内核态切换到用户态;

  • runloop控制线程保活

# 启动线程永驻
self.thread = [[NSThread alloc] initWithBlock:^{
        NSLog(@"begain a  thread ---------------");
        // 添加一个port任务 不至于让thread因为没有任务导致退出
        [[NSRunLoop currentRunLoop] addPort:NSPort.new forMode:NSDefaultRunLoopMode];
        //每执行完一个任务 这个循环就会重新执行一次
        while (!weakself.isStopThread && weakself != nil) {
            //关键点:runloop内部会开一个无限循环  有任务做事  无任务休眠; 默认在每执行一次任务就跳出内部循环
            bool result = [NSRunLoop.currentRunLoop runMode:NSDefaultRunLoopMode beforeDate:NSDate.distantFuture];
            NSLog(@"%@",result ? @"runloop 运行成功" : @"runloop 运行失败");
        }
        NSLog(@"end a thread  -----------------");
    }];
[self.thread start];

#执行任务
if (self.thread) {
        //waitUntilDone: 等到任务执行完才往下走
        [self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:true];
}

# 停止线程
// 使用thread执行这个方法
 - (void)stopThread {
    
    NSLog(@"%s  %@", __func__, NSThread.currentThread);
    CFRunLoopStop(CFRunLoopGetCurrent());
 
    _stopThread = true;
    self.thread = nil;
}
  1. 多线程

  • 多线程的使用方式有四种:


    image.png
  • 线程的队列执行的方式分为同步和异步两种,同步执行不会重新创建线程,就在当前线程执行。异步执行在新的线程中执行,可能会创建新的线程(在主队列就不会创建新的线程);同步异步主要的区别是会不会创建新的线程;

  • 队列的类型分为串行和并发队列,串行队列就是一个个去执行任务,异步队列就会有多个线程同时去执行任务;


    image.png
  • 死锁的情况:如果在串行队列queue_A中的任务S1中使用sync添加同步任务S2并且该任务的执行队列依然是queue_A,就会产生死锁。本质就是因为S1要执行完任务必须得执行完任务S2,但是由于S2是在S1中执行,由于S1跟S2在同一个同步队列中,所以S2要想执行,必须等S1执行完。因此就产生了相互等待,就死锁了;

同步队列queue_A
S1任务 S2属于S1中要执行的任务
S2任务 S2也是一个任务,但是它必须排在S1后面执行,同时S1又必须得等S2执行完
S3任务
S4任务
......
  • GCD中group的使用
如果想在一个并发队列中先执行S1和S1任务,等这个任务执行完之后再执行S3。
1.  创建一个group和一个并发队列queue;
2. 将S1和S2分别添加到queue和group中;
3. 在使用group的notify执行S3;
  • 线程同步方案:
  1. OSSpinLock——自旋锁,是一种忙等的锁,相当于写了一个while循环,会一直占用cpu资源。;
  • 初始化:OS_SPINKLOCK_INIT
  • 尝试加锁:OSSpinLockTry
  • 加锁:OSSpinkLockLock
  • 解锁:OSSpinkLockUnlock

目前这把锁并不是安全的,它会出现线程优先级反转的问题。因为多线程在并发执行的时候,是cpu每一个线程轮流分配一点时间,只是这个时间分配的非常的短,感觉像是同时执行的。然而在CPU在分配给线程的时间依赖于线程的优先级,如果优先级高CPU分配给该线程执行任务的时间会更长。所以讲线程A和线程B在同时执行同一个任务的时候,如果线程A的优先级高于线程B,在线程B加锁后,线程A此时进来发现被加锁了,会在原地一直等待(会一直占用CPU资源)。这是由于线程A的优先级更高,所以cpu会一直分配资源给线程A执行任务。这个时候线程B由于优先级低导致没有资源科执行任务,也就导致当前的任务执行不完,线程B也无法释放锁。最终可能形成死锁;

  1. os_unfair_lock - 为了替代OSSpinkLock,在iOS10.0以后出现的一种锁,它解决了OSSpinkLock优先级反转的问题,在碰到加锁的时候,不会去忙等,而是睡眠;
  • 初始化:os_unfair_lock_init
  • 尝试加锁:os_unfair_lock_try
  • 加锁:os_unfair_lock_lock
  • 解锁:os_unfair_lock_unlock
  1. pthread_mutex——从名字来看叫做互斥锁,在线程等待的时候是睡眠处理。互斥锁还有另外一种类型PTHREAD_MUTEX_RECURSIVE是递归锁,它的特性是同一个线程可以重复的加锁开锁,如果不是同一线程就会产生互
    斥。
  • 引用库:pthread.h

  • 初始化:pthread_mutex_init(*, null) 或者PTHREAD_MUTEX_INITIALIZER, 后者是一个结构体宏定义

  • 尝试加锁:pthread_mutex_trylock

  • 加锁:pthread_mutex_lock

  • 解锁:pthread_mutex_unlock

  • 条件:pthread_cond_t,可以做到解锁一个线程然后开始睡眠,直到在另外一个线程中通知pthread_cond_t控制睡眠的线程醒来继续加锁并执行。
    加锁睡眠:pthread_cond_wait 通知解锁睡眠: pthread_cond_signal(通知单个线程) / pthread_cond_broadcast(通知多个线程)。
    最后还需要pthread_cond_destroy销毁;

  • 销毁:pthread_mutex_destroy

  1. disptach_semaphore——信号量,用来控制线程最大并发数量
  • 创建信号量:dispatch_semephore_create(最大并发数量);
  • 开始等待:dispatch_semaphore_wait 就是让信号值减1,如果信号值等于0的时候就会让线程休眠等待,直到信号量的值大于0;
  • 信号记录:dispatch_semaphore_signal 让信号值加1,并且通知等待的地方;
  1. 串行队列同步执行——将多个线程任务放到同一个队列中执行;
  2. NSLock 是对mutext普通锁的封装
  3. NSRecursiveLock 是对mutext递归锁的封装
  4. NSCondition 是对pthread_cond和mutex的封装
  5. NSConditionLock 是对NSCondition进一步的封装
  6. synchronized——是对mutex的封装,@synchornized(以某个对象为加锁对象){}。在底层实际上是使用传进去的对象生成另外一个对象,该对象维护的就是一个递归锁;
  • 这些锁的性能由高到低排列是:
同步方案的性能表现
us_unfair_lock
OSSPinkLock
dispatch_semaphore
pthread_mutex
dispatch_queue(DISPATCH_QUEUE_SERIAL)
NSLock
NSCondition
pthread_mutext(recursive)
NSRecursiveLock
NSConditionlock
@synchronized
  • 修饰属性关键字atomic
    如果使用@property(atomic)定义一个属性A,那么amotic,会在setter和getter方法内部加一个锁,以保证设置属性和获取属性是线程安全的;
  • 多读单写的解决方案, 多读单写的要求是多个线程可以并发进行读操作,只允许同时一个线程进行写操作,并且在写操作的时候,不能进行读操作;
    1. 读写锁:pthread_rwlock,读的时候使用pthread_rwlock_rdlock加锁,写的时候使用pthread_rwlock_wrlock加锁;
    2. 在写操作的时候设立屏障,使用dispatch_barrier_sync(queue),在读操作的时候使用dispatch_async(queue),读写任务必须在同一个队列,而且该队列必须是自己创建的。如果是全局队列或者是串行队列,设立屏障没有效果;
  1. 内存管理

  • iOS程序的内存分布:内存被一段一段的分隔开来,从低到高可分为:保留区域,代码区域、数据区域、堆区、栈区、内核区;

    1. 保留区域取决于硬件配置,该区域不能使用;
    2. 代码区域就是存放编译的代码,所以iOS项目代码大小是有限制的;
    3. 数据区域存放着字符串常量,已初始化和未初始化的全局变量和静态变量;
    4. 堆区就是可以有程序员控制的区域,那些通过alloc、malloc、calloc动态分配的空间。分配地址是从低到高;
    5. 栈区主要是函数中产生的开销,比如局部变量。分配的地址从高到低;
    6. 内核区系统底层用的区域;
  • Tagged Pointer 标记指针。在64位架构下,对NSNumber、NSString、NSDate小对象进行了优化。将这些类型的数据和类型直接存储到了指针中,不需要动态分配内存,维护引用计数等。当数据在指针中放不下时,才会动态分配内存。如果调用方法,也是调用objc_sendMessage,显然标记指针对象没有isa,所以是直接从指针地址中取出对象的数据,大大的提高了效率。
    如果对象指针地址中的二进制的最低或高位(Mac平台是最低位1,iOS平台是最高位1 << 63)是1时,就证明该指针是标记指针

  • OC对象的内存管理

    • 根据对象的引用计数器进行内存管理,当对象的计数器为0的时候就释放对象
    • xCode支持MRC和ARC两种管理内存模式,MRC就是手动管理内存,需要自己对对象进行retain、release、autorelease操作。而ARC就是自动管理内存,在xcode编译的时候会在对象需要的地方自动添加retain、release和autorelease代码;retain对象引用计数器+1,release对象引用计数器-1,autorelease在适当的时候引用计数器自动-1。当调用方法alloc、new、copy、mutableCopy返回一个对象时,对象的引用计数器就是1;
    • OC属性中的关键字,assign == 直接赋值、retain/strong == 在赋值的时候对新值retain对旧值release、copy == 在赋值的时候对旧值release对新值copy;
  • @autoreleasepool自动释放池实现原理:


    image.png
    • autoreleasepool实际是会转成AutoReleasePoolPage对象,每一个autoreleasepool的大小是4096个字节,其中56个字节是存放成员变量的,剩余的就是存放调用了autorelease的对象地址。如果存满就会创建下一个AutoReleasePoolPage对象,并且上一个page对象的child会指向新创建的page,新创建的page就指向上一个page对象。所以它的整体设计是一个双向链表的结构;
    • @autoreleasepool是可以嵌套的,每个@autoreleasepool一开始就会调用objc_autoreleasePoolPush,,如果没有page对象就创建一个,并且往栈内放一个POOL_BOUNDARY(其实就是个0),并且返回这个栈顶的地址。每个@autoreleasepool最后都会调用objc_autoreleasePoolPop(),将一开始放进去的POOL_BOUNDARY的地址传进去,表示释放到这个位置,然后就开始释放栈内的地址,直到POOL_BOUNDARY。
      查看autoreleasepool内的情况,通过声明C函数extern void _objc_autoreleasePoolPrint()
    • autorelease的对象在什么时候释放呢?在主线程的runloop中有两个observer,一个是监听runloop睡眠之前的状态,还有一个是runloop的退出状态,这两个observer都会调用一次objc_autoreleasePoolPop释放对象。在睡眠之前的状态则还会调用一次objc_autoreleasePoolPush;
  1. 性能优化

  • 在屏幕成像的过程中,CPU和GPU起着至关重要的作用。
    CPU主要负责对象的创建销毁、对象属性的调整计算、布局的计算、文本的计算和排版、图片的格式转换和解码、图像的绘制等;
    GPU主要负责纹理的渲染,在CPU计算完的数据会交给GPU去处理成渲染数据,并且放到一个缓冲区内(在iOS中有两个缓冲区),每收到一次垂直同步信号,就会从缓冲区内取一帧的数据进行渲染;
  • 屏幕卡顿的原因:

iOS的屏幕渲染是每秒60帧,也就是每过16毫秒就会收到一次垂直同步信号。在每一次收到垂直信号的时候,如果帧缓冲区内没有新的数据,就会显示上一次的数据,这就导致掉帧了;

  • 针对CPU的优化:
  1. 尽量用轻量级的对象,比如不需要处理事件的图层,能用CALayer就不用UIView;
  2. 不要频繁的调用UIView的相关属性,比如frame、bounds、transform等,尽量减少不必要的修改;
  3. 尽量提前计算好布局。在需要时一次性调整对象的属性,不要多次修改属性;
  4. Autolayout布局比frame更耗性能;
  5. 图片的size最好跟UIImageView的size保持一致,避免重绘带来的性能损耗;
  6. 控制线程的最大并发数;
  7. 尽量把耗时的操作放到子线程,比如文本的尺寸计算,绘制,图片的解码和绘制;
  • 针对GPU的优化:
  1. 尽量避免短时间内显示大量的图片,尽可能的多张图片合成一张图片进行展示;
  2. GPU能处理的最大纹理尺寸是4096*4096,一旦超过这个尺寸,就会占用CPU的资源来处理,所以纹理尽量不要超过这个尺寸;
  3. 尽量减少视图的数量和层次;
  4. 减少使用透明的视图,不透明的就设置opaque= true;
  5. 尽量避免离屏渲染;
  • 离屏渲染
  1. 在openGL中,GPU有两种渲染方式:
    1. 当前屏幕渲染:在当前用于显示的屏幕缓冲区进行渲染操作;
    2. 离屏渲染:在当前屏幕缓冲区意外另外开辟一个缓冲区进行渲染操作;
  2. 离屏渲染因为需要创建新的缓冲区,并且在屏幕渲染的过程中,需要要多次切换上下文环境,先是从当前屏幕切换到离屏缓冲区,等到离屏结束后,将离屏缓冲区的渲染结果渲染到屏幕上,又需要切回当前屏幕缓冲区,这样的操作比较消耗性能;
  3. 触发离屏渲染的操作有:
    1. 光栅化,layer.shouldRasterize = true;
    2. 遮罩,layer.mask;
    3. 圆角,同时设置layer.masksToBounds = true, layer.cornerRadius > 0;
    4. layer.shadowXXX, 如果设置layer.shadowPath就不会产生离屏渲染;
  • 耗电优化
  1. 少用定时器;
  2. 优化I/O操作,不要频繁写入小数据,最好批量一次性写入。读写大量重要数据时,可以考虑用dispatch_io,其提供了基于GCD的异步操作文件I/O的API,用dispatch_io系统干回优化磁盘访问。数据量比较大的,应该使用数据库;
  • 网络优化:
    1. 减少、压缩网络数据(json、protobuffer);
    2. 如果多次请求的结果是相同的,尽量使用缓存;
    3. 使用断点续传,否则网络不稳定时,可能多次传输相同的内容;
    4. 网络不可用时,不要尝试执行网络请求;
    5. 让用户可以取消长时间运行或者速度很慢的网络操作,设置合适的超时时间;
    6. 批量传输:比如下载视频流量时,不要传输很小的数据包,直接下载整个文件或者一大块一大块的下载;
  • 定位优化:
    1. 如果只需要快速定位确定用户的位置,最好用CLLOcationManager的requestLocation方法。因为定位完成后,会自动让定位硬件断电;
    2. 如果不是导航应用,尽量不要实时更新位置,定位完毕后就关闭定位服务;
    3. 尽量降低定位精度,比如金莲更不要使用精度最高的KCLLocationAccuracyBest;
    4. 需要后台定位时,尽量设置pauseLocationUpdatesAutomatically为true,如果用户不太可能移动的时候系统会自动暂停位置更新;
  • App启动优化
    • App的启动分为两种,一种是冷启动,就是从零开始启动app,第二种是热启动,App在后台的时候再次启动App。 启动优化主要针对前者;
    • 可以通过设置环境变量可以打印出app的启动时间分析,Edit scheme —> Run —> Arguments —> arguments passed on launch,下面添加变量DYLD_PRINT_STATISTICS并设置为1,如果需要更详细的信息DYLD_PRINT_STATISTICS_DETAIL;
    • App启动分为三个阶段,先后分为:dyld、runtime、main函数
      1. Dyld,Apple的动态链接器,可以用来装载Mach-O文件(可执行文件、动态库等);
        1. Dyld会装载App的可执行文件,同时会递归连接所有依赖的动态库。当dyld把可执行文件、动态库都装载完毕后,会通知runtime进行下一步的处理;
        2. 优化方案:
          1. 减少动态库。合并一些动态库,定期清理不必要的动态库;
          2. 减少objc类和分类的数量、减少selector数量,定期清理不必要的类和分类;
          3. 减少C++虚函数的数量;
          4. swift尽量使用struct;
      2. Runtime会调用map_images进行可执行文件内容的解析和处理,在load_images中调用call_load_methods,调用所有class和category的load方进行各种objc结构的初始化(注册objc类、初始化类对象等等)。还会调用C++静态初始化器和atrribute修饰的函数。 这样可执行文件和动态库中所有的class、protocol、selector、、IMP都已经按格式成功加载到内存中,被runtime所管理;
        优化方案:
        用+initialize方法配合dispatch_once替代attribute((constructor))、C++静态构造器、Objc的load方法
      3. Main,App的启动由dyld主导,将可执行文件加载到内存,顺便加载所有依赖的动态库,并有runtime负责加载成objc定义的结构。所有的初始化结束后,dyld就会调用main函数,再调用UIApplicationMain函数,APPDelegate的application:didFinishedLaunchingWithOptions方法。
        优化方案:
        在不影响用户的前提下,尽量将一些耗时操作延迟,不要全部放到finishLoading方法中;
  • 安装包瘦身:
  • 安装包主要有可执行文件、资源(图片、音频、视频等)组成;
  • 资源可以采用无损压缩;
  • 去除没有用到的资源,使用开源项目:https://github.com/tinymind/LSUnusedResources
  • 编译器优化(瘦身可执行文件)
  • 将工程的Strip lInked Product 、Make Strinfs read-only、Symbols Hidden by Default 设置为true(新的Xcode项目这些值默认就是true),可以去除不必要的调试符号;
  • 去掉异常支持,Enable C++ Exception、Enable Objective-Exception设置为false、Other C Flags添加-fno-exceptions(实际验证这一步并没有什么卵用)
  • 利用AppCode(IDE)(https://www.jetbrains.com/objc/)检测未使用的代码:菜单栏 -> Code -> Inspect Code
  • LinkMap- 查看每个函数占用的大小
image.png
  1. 架构设计

  • 架构属于软件设计方案,具体可到类与类之间的关系、模块与模块之间的关系、客户端与服务端之间的关系。在开发中常用的有MVC、MVP、MVVM,这三种构架都是view和model相互解耦,可独立重复利用。并且都是以controller、presenter、viewModel为媒介处理view层的需要;

  • 一般来说架构可以分成:三层架构和四层架构。
    三层架构:界面层、业务层、数据层;
    四层架构:界面层、业务层、网络层、数据层;
    前面的MVC、MVP、MVVM,通产常用于界面层的设计使用;
    优秀博客地址:https://github.com/skyming/Trip-to-iOS-Design-Patterns

  • 设计模式

    image.png

    优秀博客地址:https://design-patterns.readthedoc.io/zh_CN/latest

  1. 数据结构和算法

推荐书籍:
严蔚敏的《数据结构》
《大话数据结构和算法》

  1. 网络

推荐书籍:
《HTTP权威指南》
《TCP/IP详解卷1:协议》

持续更新 学无止境

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容