iOS底层基本知识点面试问题

469 阅读16分钟

objc源码:opensource.apple.com/source/objc…

一些命令:

  1. oc文件转c++命令:xcrun -sdk -iphoneos clang -arch arm64 -rewrite-objc xxx.m
  2. 命令行生成LLVM中间代码(IR): clang -emit-llvm -S xxx.m

OC对象本质

通过点击Class进入内部,我们可以发现Class对象是一个指向objc_class结构体的指针。

我们首先看一下objc_class结构体:

struct objc_class {
    Class isa;  
    Class super_class;
    cache_t cache;             //方法缓存
    class_data_bits_t bits;//用于获取具体的类信息class_rw_t *data = bits & FAST_DATA_MASK
}

注:class_rw_t是个保存类信息的结构体,如下

struct class_rw_t {
    uint32_t flags;
    uint16_t version;
    uint16_t witness;

    const class_ro_t *ro;   //存储编译时当前类已经确定的属性、方法、协议的信息

    method_array_t methods;      //方法列表
    property_array_t properties; //属性列表
    protocol_array_t protocols;  //协议列表

    Class firstSubclass;
    Class nextSiblingClass;

    char *demangledName;
}

注:class_ro_t是个保存编译时类已确定的信息的结构体,如下

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize; //instance对象占用的内存空间
#ifdef __LP64__
    uint32_t reserved;
#endif

    const uint8_t * ivarLayout;
    
    const char * name;  //类名
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;  //成员变量列表

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;
}
1. 一个NSObject对象占用多少内存?

占用一个指针变量所占用的大小(64bit,8个字节,32bit,4个字节)
系统分配了16个字节给NSObject对象(通过malloc_size函数获得)
但NSObject对象内部只使用了8个字节的空间(64bit环境下,可以通过class_getInstanceSize函数获得)

2. 对象的isa指针指向哪里?

instance对象的isa指向class对象。
class对象的isa指向meta-class对象。
meta-class对象的isa指向基类的meta-class对象

3. OC的类信息存放在哪里?

对象方法、属性、成员变量、协议信息,存放在class对象中
类方法,存放在meta-class对象中
(因为这些编译器只需编译一次就可以知道类里面有什么东西了)
----------------------------------
成员变量的具体值,存放在instance对象里

4. class对象superclass的指针作用,meta-class对象的superclass指针的作用
@interface Person: NSObject
- (void)run;
+ (void)personClassMethod;
@end
Class Student: Person
@end

Student *student = [[Student alloc] init];
[student run];
[Student personClassMethod];

调用方法其实就是给对象发消息

class对象superclass指针: 实例方法是存在类对象里的,首先先通过student实例对象的isa找到student自己的类对象,然后通过Sutdent类对象的superclass指针找到Person类对象,然后就调起Person类对象里面的run方法

meta-class: 首先类方法时存放在元类对象里的,当Student的class要调用Person的类方法时,首先通过Student的class的isa找到Student的meta-class,然后通过superclass找到Person的meta-class,最后找到类方法实现进行调用

5. 简述OC方法的调用过程

oc是动态语言,在运行时方法会被动态转为消息发送,即:objc_msgSend(receiver, selector)。在方法调用的时候,其实就是给某个对象发送消息。
1.runtime会根据对象的isa找到对象所属的类,然后在类的方法列表里寻找,如果找到相符的方法的话,就调用这个方法的实现。
2.如果在当前所属类找不到的话,就通过superclass指针向父类的方法列表中寻找方法并调用。
3.如果在最顶层的父类(一般是NSObject)中找不到的话,程序会抛出异常unrecognized selector sent to xxx,

KVO

1. iOS用什么方式实现一个对象的KVO?(KVO本质、原理是什么?)
  • 利用runtime的API(objc_allocateClassPairobjc_registerClassPair)动态生成一个子类,并且让instance的isa指向这个全新的子类(NSKVONotifying_XXX)
  • 当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify函数

_NSSetXXXValueAndNotify函数里会调用:

1. willChangeValueForKey:       
2. 父类原来的setter方法        
3. didChangeValueForKey:   
内部会触发监听器(Observer)的监听方法(observeValueForKeyPath: ofObject:(id)object change: context:)

KVO本质其实就是重写了set方法。
KVO的原理其实就是把addObserver的instance的isa指向了NSKVONotifying_XXX的类,而这个类里面重写了set方法的实现。

2. 如何手动触发KVO?

手动调用willChangeValueForKey:didChangeValueForKey:

3. 直接修改成员变量会触发KVO吗?

不会。因为只接修改成员变量(_age = 2)不会触发set方法。

Category分析

1. Category的加载处理过程。

1.通过Runtime加载某个类的所有分类数据。
2.把所有Category的方法、熟悉、协议数据,合并到一个大数组中,后面参与编译的Category数据,会在数组的前面。
3.将合并后的分类数据(方法、熟悉、协议)插入到类原来数据的前面。
注:也就是说调用方法时默认会找到最新添加的Category的方法调用。(可以通过xcode的compile sources修改编译顺序)

2.分类的实现原理。

Category在编译后会生成一个category_t对象,有多个分类的时候就会有多个category_t类型的对象,在编译阶段分类里面的东西跟类的东西是分开的,在运行的时候,分类的东西会合并到类(如:对象方法等)或者元类(如:类方法)里面去。

分类编译成struct category_t,里面存储着分类的对象方法,类方法,属性,协议信息。结构如下:

struct _category_t {
	const char *name;                              //分类名称,如:Person
	struct _class_t *cls;                          // 0
	const struct _method_list_t *instance_methods; //对象方法列表
	const struct _method_list_t *class_methods;    //类方法列表
	const struct _protocol_list_t *protocols;      //协议列表
	const struct _prop_list_t *properties;         //属性列表
};

+load, +initialize

1. +load, +initialize方法的区别是什么?
  1. 调用方式
    +load是根据函数地址直接调用。(*load_method)(cls, @selector(load))
    +initialize是通过objc_msgsend调用。
  2. 调用时刻
    +loadruntime加载类、分类的时候调用(只会调用1次)
    +initialize是类第一次接受到消息的时候调用,每一个类只会initialize一次(父类的initialize可能会被调用多次,因为当给子类发消息时,如果子类没有实现+initialize,isa在元类中找不到方法,然后会通过superclass指针找到父类的+initialize)。
2. +load, +initialize方法的区别是什么?
  1. +load
    (1) 先调用类的+load:先编译的类,优先调用+load,调用子类的+load之前会先调用父类的+load
    (2) 再调用分类的+load:先编译的分类,优先调用+load
  2. +initialize (1)先初始化父类
    (2)再初始化子类(可能最终调用的是父类的initialize方法)

Block

首先要知道的内容:

栈:内存会自动销毁的(执行完当前方法之后)
堆:需要手动申请和释放内存(releasefree()

auto变量:auto int aauto是在OC语法中默认省略了的

Block有3种类型: __NSGlobleBlock____NSStackBlock__NSMallocBlock__

疑问:

1.怎么判断block是哪种类型?
类型 环境
__NSGlobleBlock__ 没有访问auto变量
__NSStackBlock__ 访问了auto变量
__NSMallocBlock__ __NSStackBlock__调用了copy
2. __block的内存管理

block在栈上时,并不会对__block变量产生强引用。
blockcopy到堆时,会调用block内部的copy函数,然后copy函数内部会调用_Block_object_assign函数,而_Block_object_assign函数会对__block变量形成强引用(retain)

3. __weak__unsafe_unretained

__weak: 不会产生强引用,指向的对象销毁时会自动让指针置为nil。
__unsafe_unretained: 不会产生强引用,不安全,指向的对象销毁时,指针存储的地址值不变,访问的时候会出现野指针

4. 解决block的循环引用问题---ARC

5. block的本质是什么?

block本质上也是一个OC对象,内部封装了函数调用以及函数调用环境(函数调用地址、变量)。

6.__block修饰符的作用是什么?

__block修饰的变量会被编译器包装成一个__Block_byre_xx_0对象,可以用于解决block内部无法修改auto变量值的问题。

7. block的属性修饰词为什么用copy?使用block有哪些注意点?

block一旦没有进行copy操作,就不会在堆上。因为只有在堆上的时候才能控制block的生命周期,才能进行内存管理

Runtime

1.讲一下消息机制。objc_msgSend流程。

源码阅读顺序:

OC中的方法调用其实就是objc_msgSend的调用,它会给receiver(方法调用者)发送一条消息(selector方法名),objc_msgSend底层有三个阶段:
消息发送->动态方法解析->消息转发

**消息发送阶段流程**   
执行`objc_msgSend`后
1. 首先会判断`receiver`是否为`nil`,   
2. (否)然后会通过`isa`指针找到类对象`(reveiverClass)`再从中的`cache`中查找方法,  
3. (没找到)然后会去`class_rw_t`中查找方法,     
4. (没找到)然后会通过`receiverClass`的`superclass`指针找到`superClass`再从`cache`中查找
5. (没找到)再去这个`superClass`的`class_rw_t`中查找方法
一直重复4跟5的步骤,找到了方法就结束查找并将方法缓存到`receiverClass`的`cache`中。

假如在最上层的`superClass`还没找到方法,就会走下一个流程:

**动态方法解析流程**   
1. 首先会判断是否有过动态解析, 有的话会直接走**消息转发流程**
2. (否)会调用`+resolveInstanceMethod`或`+resolveClassMethod`方法来动态解析方法(这方法的实现里面可以通过`class_addMethod`添加方法)
3. 标记已经动态解析(这里为了如果动态解析完成后,再走回消息发送流程还是找不到方法实现,就继续走动态分析解析流程的判断1)
动态解析过后会重新走 >> **消息发送流程**(如果还是找不到方法实现,就会走**消息转发流程**)

**消息转发流程**
当上面两个流程都找不到方法的话,进入消息转发流程,首先会调用
1. `forwardingTargetForSelector:`
返回值不为nil的话,
会调用`objc_msgSend(返回值, SEL)`,让别人进行调用
返回值为nil的话,
2. 会调用`methodSignatureForSelector`方法返回方法签名,
返回值不为nil的话,
会调用`forwardInvocation:`的方法,里面可以随便处理想做的事情
返回值为`nil`的话,
会调用`doesNotRecognizeSelector`方法(就会在控制台报方法找不到的错误)
2. 什么是runtime?项目中哪里用到?

首先OC是一门动态性比较强的编程语言,它允许很多操作推迟到程序运行时再进行,OC的动态性是由Runtime来支撑和实现的,Runtime是一套C语言的API,封装了很多动态性相关的函数,平时我们编写的OC代码都是转换成Runtime API进行调用。
具体应用:
1.利用关联对象(AsscoiatedObject)在分类添加属性。
2.遍历类的所有成员变量(class_copyIvarList), 字典转模型里用到。
3.交换方法实现(method_exchangeImplementations), 页面统计、拦截Button点击事件(sendAction:to:ForEvent:)时用到。
4.利用消息转发机制解决方法找不到的异常问题。

sendAction:to:ForEvent::点击Button时会先触发这个方法再触发selector,可以在这个方法里面处理要不要触发selector方法

3. isa

从arm64架构开始,isa经过优化变成了一个(union)共用体结构,它的8个字节不仅仅存放Class, Meta-Class地址值,还存放着更多的信息。

//arm64
union isa_t {
    Class cls;
    uintptr_t bits;
    struct {
      uintptr_t nonpointer        : 1;                                       \
      uintptr_t has_assoc         : 1;                                       \
      uintptr_t has_cxx_dtor      : 1;                                       \
      uintptr_t shiftcls          : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
      uintptr_t magic             : 6;                                       \
      uintptr_t weakly_referenced : 1;                                       \
      uintptr_t deallocating      : 1;                                       \
      uintptr_t has_sidetable_rc  : 1;                                       \
      uintptr_t extra_rc          : 19
    };
};

Runloop

1. Runloop与线程
  • 每条线程都有唯一的一个与之对应的Runloop对象
  • Runloop保存在一个全局的Dictionary里,线程为key,Runloop为value
  • 线程刚创建时并没有Runloop对象,Runloop在第一次获取 它时创建
  • Runloop会在线程结束时销毁
  • 主线程的Runloop已经自动创建,子线程默认没有开启Runloop
2. Runloop的运行逻辑
Source0:
触摸事件处理
performSelector:onThread:

Source1:
基于port的线程间通信
系统事件捕捉

Timers:
NStimer
performSelector:withObject:afterDelay:

Observers:
用于监听Runloop的状态
UI刷新
AutoreleasePool

流程:
1.通知Observers: 进入loop
2.通知Observers: 即将处理Timers
3.通知Observers:即将处理Sources
4.处理Blocks
5.处理Source0(可能会再次处理Blocks)
6.如果存在Source1,就跳转到第8步
7.通知Observers: 开始休眠(等待消息唤醒)
8.通知Observers: 结束休眠(被某个消息唤醒)
---或(1)处理Timer
---或(2)处理GCD Async To MainQueue
---或(3)处理Source1
9.处理Blocks
10.根据前面的执行结果,决定如何操作
---(1)回到第二部
---(2)退出loop
11.通知Observers: 退出loop

3. performSelecter:afterDelay: 本质原理

本质是会内部会创建一个定时器,并添加到当前线程的Runloop中,如果当前线程没有启动Runloop(子线程默认没有启动Runloop),该方法会失效。

4. Runloop 基本作用

1.保持程序的持续运行
2.处理APP中的各种事件(比如触摸事件、定时器事件等)
3.节省CPU资源,提高性能:该做事时做事,该休息时休息 ...

多线程

常见多线程方案:

1. 多线程中容易混淆的术语:同步/异步,并发/串行

1.同步异步主要影响: 能不能开启新线程
同步:在当前线程中执行任务,不具备开启新线程能力
异步:在新的线程中执行任务,不具备开启新线程能力
2.并发串行主要影响:任务的执行方式
并发:多个任务并发(同时)执行
串行:一个任务执行完后,再执行下一个

使用sync函数往当前串行队列中添加任务,会卡住当前串行队列(产生死锁)

2. 线程同步的方案(性能从高到低)

os_unfair_lock(ios10),
OSSpinLock(10之前)(自旋锁),
dispatch_semaphore,
pthread_mutex(互斥锁), dispatch_queue(DISPATCH_QUEUE_SERIAL),
NSLock,
NSCondition,
pthread_mutex(rescursive)(递归锁),
NSRecursiveLock(递归锁),
NSConditionLock,
@synchronized

自旋锁:等待锁的线程会处于忙等状态,一直占用CPU资源
互斥锁:等待锁的线程会处于休眠状态

文件读写:
1.用pthread_rwlock可以解决读写安全的问题。
同一时间,只能有1个线程进行写的操作
同一时间,允许有多个线程进行读的操作
同一时间,不允许既有写,又有读的操作。

2.用`dispatch_barrier_async`
dispatch_async(queue, ^{
    //读
});
dispatch_barrier_async(queue, ^{
    //写
})

内存管理

内存布局从低到高:代码段->数据段->堆->栈

1. 深拷贝和浅拷贝

拷贝本质:产生一份副本对象,跟源对象互不影响
深拷贝:内容拷贝,会产生新对象
浅拷贝:指针拷贝,不产生新对象

1. oc对象的内存管理

在iOS中,是使用引用计数来管理OC对象的内存的
一个新创建的OC对象引用计数默认为1,当引用计数减为0后,OC对象就会销毁,释放占用的内存空间
调用retain会让OC对象的引用计数+1,调用release会让OC对象的引用计数-1

2. weak指针的实现原理

它会将我们这些弱引用存到一个哈希表里边,到时候对象销毁的时候,就会取出当前对象对应的弱引用表,把弱引用表里面的弱引用都清除掉

3. Runloop与Autorelease

ios在主线程的Runloop中注册了2个Observer,
第一个Observer监听了kcfRunloopEntry事件,调用objc_autoreleasePush()
第二个Observer
监听了kCFRunloopBefreWaiting休眠事件,会调用objc_autoreleasePoolPop()objc_autoreleasePush() 监听了kCFRunloopBefreExit退出事件,会调用objc_autoreleasePoolPop()

性能优化

1. 卡顿优化

① CPU

  • 尽量用轻量级的对象,比如用不到事件处理的地方,考虑使用CALayer取代UIView
  • 不要频繁地调用UIView的相关属性,比如frameboundstransform等属性,尽量减少不必要的修改
  • 尽量提前计算好布局,在有需要时一次性调整对应的属性,不要多次修改属性
  • Autolayout会比直接设置frame消耗更多的CPU资源
  • 图片size最好跟UIImageView的size保持一致
  • 控制一下线程的最大并发数
  • 尽量把耗时的操作放到子线程,例如:文本处理(尺寸计算、绘制),图片处理(解码、绘制)。

② GPU

  • 尽量避免短时间内大量图片的显示,尽可能将多张图片合成一张进行显示
  • GPU能处理的最大纹理尺寸为4096x4096,一旦超过这个尺寸,就会占用CPU资源进行处理,所以纹理尽量不要超过这个尺寸
  • 尽量减少视图数量和层次
  • 减少透明的视图(alpha<1),不透明就设置opaque为YES
  • 尽量避免出现离屏渲染, 例如:光栅化(Ratersize),遮罩(mask),圆角(cornerRadius,尽量考虑通过CoreGraphic绘制裁剪圆角),阴影(shadowXXX,如果设置了layer.shadowPath就不会产生离屏渲染)

卡顿原因
平时我们所说的卡顿,主要是因为在主线程执行了比较耗时的操作
卡顿检测:
可以添加Observer到主线程Runloop中,通过监听Runloop状态切换的耗时(即监听休眠结束以后,处理source1到source0中间的时间),以达到监控卡顿的目的

2. APP冷启动过程

dyld(动态链接器) -> runtime -> main:启动三大阶段。
APP的启动由dyld主导,将可执行文件加载到内存,顺便加载所有依赖的动态库。
并由runtime负责加载成objc定义的结构
所有初始化工作结束后,dyld就会调用main函数。

3. APP启动优化

按照不同的阶段:
dyld阶段
1.减少动态库、合并一些动态库(定期清理不必要动态库)
2.减少Objc类、分类的数量、减少Selector数量(定期清理不必要的类和分类)
3.减少C++虚函数数量
4.Swift尽量使用struct
...
runtime阶段:
+initialize方法和dispatch_once取代所有__attribute__((constructor))C++静态构造器、Objc+load
...
main阶段:
在不影响用户体验的前提下,尽可能将一些操作延迟,不要全部都放在finishlaunching方法中

4. 安装包瘦身

安装包(IPA)主要由可执行文件、资源组成。
资源(图片、音频、视频等)
1.采取无损压缩
2.去除无用资源,可用LSUnusedResources查找
...
可执行文件瘦身
去掉异常支持:Enable C++ ExceptionsEnable Objective-C Exceptions设为NO,Other C Flags添加-fno-exceptions.
利用AppCode检测未使用的代码,菜单 -> Code -> Inspect Code.
或编写LLVM插件检测未使用的代码、重复代码.
...
LinkMap
生成LinkMap文件,可以查看可执行文件的具体组成。
Build Setting -> Link Map -> Write Link Map File => Yes,生成的LinkMap文件路径修改放到桌面。可用工具LinkMap解析

架构

1. MVC/MVVM/MVP
苹果官方MVC:
优点:model、view可重复利用
缺点:controller过于臃肿

变种后MVC:
优点:对controller进行了瘦身,将view内部细节封装了起来,外面不知道view的具体实现
缺点:view依赖于Model
2. 三层架构

界面层、业务层、数据层。
我们平时所说的MVC/MVVM等等都是我们界面层的架构。

未完,待续。

本文供本人用来回忆所用,知识点理解没写完整,具体理解自己看