iOS底层原理学习笔记

作者: 东也_ | 来源:发表于2021-07-23 12:34 被阅读0次
    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:协议》

    持续更新 学无止境

    相关文章

      网友评论

        本文标题:iOS底层原理学习笔记

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