面试题收藏面试题iOS面试题+基础知识

iOS底层原理学习笔记

2021-07-23  本文已影响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
  1. KVO

  1. KVC

  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 
  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
  1. 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. 多线程

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

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

  1. os_unfair_lock - 为了替代OSSpinkLock,在iOS10.0以后出现的一种锁,它解决了OSSpinkLock优先级反转的问题,在碰到加锁的时候,不会去忙等,而是睡眠;
  1. pthread_mutex——从名字来看叫做互斥锁,在线程等待的时候是睡眠处理。互斥锁还有另外一种类型PTHREAD_MUTEX_RECURSIVE是递归锁,它的特性是同一个线程可以重复的加锁开锁,如果不是同一线程就会产生互
    斥。
  1. disptach_semaphore——信号量,用来控制线程最大并发数量
  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
  1. 内存管理

  1. 性能优化

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

  1. 尽量用轻量级的对象,比如不需要处理事件的图层,能用CALayer就不用UIView;
  2. 不要频繁的调用UIView的相关属性,比如frame、bounds、transform等,尽量减少不必要的修改;
  3. 尽量提前计算好布局。在需要时一次性调整对象的属性,不要多次修改属性;
  4. Autolayout布局比frame更耗性能;
  5. 图片的size最好跟UIImageView的size保持一致,避免重绘带来的性能损耗;
  6. 控制线程的最大并发数;
  7. 尽量把耗时的操作放到子线程,比如文本的尺寸计算,绘制,图片的解码和绘制;
  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系统干回优化磁盘访问。数据量比较大的,应该使用数据库;
image.png
  1. 架构设计

  1. 数据结构和算法

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

  1. 网络

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

持续更新 学无止境

上一篇 下一篇

猜你喜欢

热点阅读