0%

2020年iOS面试题总结(二)

Runtime

什么是Runtime

  • runtime是由CC++、汇编实现的一套API,为OC语言加入了面向对象、运行时的功能

  • 运行时(runtime)将数据类型的确定由编译时推迟到了运行时

  • 平时编写的OC代码,在程序运行过程中,最终会转换成runtime的C语言代码——runtimeObjective-C 的幕后作者

如类结构中的rorw属性

  • roread-only)在编译时已经确定

  • rwread-write)在运行时才确定,因此可以使用runtime进行修改

方法的本质是什么

方法的本质是发送消息objc_msgSend,即寻找IMP的过程

发送消息会有以下⼏个流程:

  • 快速查找流程 —— 通过汇编objc_msgSend查找缓存cache_t是否有imp实现

  • 慢速查找流程 —— 通过C++lookUpImpOrForward递归查找当前类和父类rwmethodlist的方法

  • 动态方法解析 —— 通过调用resolveInstanceMethodresolveClassMethod来动态方法决议——实现消息动态处理

  • 快速转发流程 —— 通过CoreFoundation来触发消息转发流程,forwardingTargetForSelector实现快速转发,由其他对象来实现处理方法

  • 慢速转发流程 —— 先调用methodSignatureForSelector获取到方法的签名,生成对应的invocation;再通过forwardInvocation来进行处理

  • 以上流程均无法挽救就崩溃并报错

SEL和IMP的关系

SEL是方法编号,也是方法名,在dyld加载镜像到内存时,通过_read_image方法加载到内存的表中了

IMP是函数实现指针,找IMP就是找函数实现的过程

SELIMP的关系就可以解释为:

  • SEL就相当于书本的⽬录标题
  • IMP就是书本的⻚码
  • 函数就是具体页码对应的内容

能否向运行时创建的类中添加实例变量

具体情况具体分析:

  • 编译好的类不能添加实例变量
  • 运行时创建的类可以添加实例变量,但若已注册到内存中就不行了

原因:

  • 编译好的实例变量存储的位置在ro,而ro是在编译时就已经确定了的
  • 编译完成,内存结构就完全确定就法修改
  • 只能修改rw中的方法或者可以通过关联对象的方式来添加属性

Category:从底层原理研究到面试题分析

利用runtime-API创建对象

API介绍

动态创建类

1
2
3
4
5
6
7
8
9
/**
*创建类
*
*superClass: 父类,传Nil会创建一个新的根类
*name: 类名
*extraBytes: 额外的内存空间,一般传0
*return:返回新类,创建失败返回Nil,如果类名已经存在,则创建失败
*/
Class FXPerson = objc_allocateClassPair([NSObject class], "LGPerson", 0);

添加成员变量

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
*添加成员变量
*这个函数只能在objc_allocateClassPair和objc_registerClassPair之前调用。不支持向现有类添加一个实例变量
*这个类不能是元类,不支持在元类中添加一个实例变量
*实例变量的最小对齐为1 << align,实例变量的最小对齐依赖于ivar的类型和机器架构。对于任何指针类型的变量,请通过log2(sizeof(pointer_type))
*
*cls 往哪个类添加
*name 添加的名字
*size 大小
*alignment 对齐处理方式
*types 签名
*/
class_addIvar(FXPerson, "fxName", sizeof(NSString *), log2(sizeof(NSString *)), "@");

注册到内存

1
2
3
4
5
6
/**
*往内存注册类
*
* cls 要注册的类
*/
objc_registerClassPair(FXPerson);

添加属性变量

1
2
3
4
5
6
7
8
9
/**
*往类里面添加属性
*
*cls 要添加属性的类
*name 属性名字
*attributes 属性的属性数组。
*attriCount 属性中属性的数量。
*/
class_addProperty(targetClass, propertyName, attrs, 4);

添加方法

1
2
3
4
5
6
7
8
9
/**
*往类里面添加方法
*
*cls 要添加方法的类
*sel 方法编号
*imp 函数实现指针
*types 签名
*/
class_addMethod(FXPerson, @selector(setHobby), (IMP)fxSetter, "v@:@");

整体使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// hobby的setter-IMP
void fxSetter(NSString *value) {
printf("%s/n",__func__);
}

// hobby的getter-IMP
NSString *fxHobby() {
return @"iOS";
}

// 添加属性变量的封装方法
void fx_class_addProperty(Class targetClass, const char *propertyName) {
objc_property_attribute_t type = { "T", [[NSString stringWithFormat:@"@\"%@\"",NSStringFromClass([NSString class])] UTF8String] }; //type
objc_property_attribute_t ownership0 = { "C", "" }; // C = copy
objc_property_attribute_t ownership = { "N", "" }; //N = nonatomic
objc_property_attribute_t backingivar = { "V", [NSString stringWithFormat:@"_%@",[NSString stringWithCString:propertyName encoding:NSUTF8StringEncoding]].UTF8String }; //variable name
objc_property_attribute_t attrs[] = {type, ownership0, ownership, backingivar};
class_addProperty(targetClass, propertyName, attrs, 4);
}

// 打印属性变量的封装方法
void fx_printerProperty(Class targetClass){
unsigned int outCount, i;
objc_property_t *properties = class_copyPropertyList(targetClass, &outCount);
for (i = 0; i < outCount; i++) {
objc_property_t property = properties[i];
fprintf(stdout, "%s %s\n", property_getName(property), property_getAttributes(property));
}
}

int main(int argc, const char * argv[]) {
@autoreleasepool {
// 动态创建类
Class FXPerson = objc_allocateClassPair([NSObject class], "FXPerson", 0);
// 添加成员变量
class_addIvar(FXPerson, "name", sizeof(NSString *), log2(sizeof(NSString *)), "@");
// 注册到内存
objc_registerClassPair(FXPerson);
// 添加属性变量
fx_class_addProperty(FXPerson, "hobby");
fx_printerProperty(FXPerson);
// 添加方法(为属性方法添加setter、getter方法)
class_addMethod(FXPerson, @selector(setHobby:), (IMP)fxSetter, "v@:@");
class_addMethod(FXPerson, @selector(hobby), (IMP)fxHobby, "@@:");

// 开始使用
id person = [FXPerson alloc];
[person setValue:@"Felix" forKey:@"name"];
NSLog(@"FXPerson的名字是:%@ 爱好是:%@", [person valueForKey:@"name"], [person valueForKey:@"hobby"]);
}
return 0;
}

注意事项

  • 记得导入<objc/runtime.h>
  • 添加成员变量class_addIvar必须在objc_registerClassPair前,因为注册到内存时ro已经确定了,不能再往ivars添加(同第四个面试题)
  • 添加属性变量class_addProperty可以在注册内存前后,因为是往rw中添加的
  • class_addProperty中“属性的属性”——nonatomic/copy是根据属性的类型变化而变化的
  • class_addProperty不会自动生成settergetter方法,因此直接调用KVC会崩溃

不只可以通过KVC打印来检验,也可以下断点查看ro、rw的结构来检验

关联对象分析

实则是为了解决分类创建属性的问题

分类直接添加属性的后果

  • 编译会出现警告:没有setter方法和getter方法
  • 运行会报错:-[FXPerson setName:]: unrecognized selector sent to instance 0x100f61180’

为什么不能直接添加属性

Categoryruntime中是用一个结构体表示的:

1
2
3
4
5
6
7
8
9
10
11
struct category_t {
const char *name;
classref_t cls;
struct method_list_t *instanceMethods;
struct method_list_t *classMethods;
struct protocol_list_t *protocols;
struct property_list_t *instanceProperties;
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties;
...
};

里面虽然可以添加属性变量,但是这些properties并不会自动生成Ivar,也就是不会有 @synthesize的作用,dyld加载期间,这些分类会被加载并patch到相应的类中。这是一个动态过程,Ivar不能动态添加.

解决方案

手动实现setter、getter方法,关联对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

- (void)setName:(NSString *)name {
/**
参数一:id object : 给哪个对象添加属性,这里要给自己添加属性,用self。
参数二:void * == id key : 属性名,根据key获取关联对象的属性的值,在objc_getAssociatedObject中通过次key获得属性的值并返回。
参数三:id value : 关联的值,也就是set方法传入的值给属性去保存。
参数四:objc_AssociationPolicy policy : 策略,属性以什么形式保存。
*/
objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSString *)name {
/**
参数一:id object : 获取哪个对象里面的关联的属性。
参数二:void * == id key : 什么属性,与objc_setAssociatedObject中的key相对应,即通过key值取出value。
*/
return objc_getAssociatedObject(self, @"name");
}

关联对象原理

setter方法——objc_setAssociatedObject分析

苹果设计接口时往往会加个中间层——即使底层实现逻辑发生变化也不会影响到对外接口

1
2
3
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) {
_object_set_associative_reference(object, (void *)key, value, policy);
}

跟进去看看_object_set_associative_reference实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
void _object_set_associative_reference(id object, void *key, id value, uintptr_t policy) {
// This code used to work when nil was passed for object and key. Some code
// probably relies on that to not crash. Check and handle it explicitly.
// rdar://problem/44094390
if (!object && !value) return;

assert(object);

if (object->getIsa()->forbidsAssociatedObjects())
_objc_fatal("objc_setAssociatedObject called on instance (%p) of class %s which does not allow associated objects", object, object_getClassName(object));

// retain the new value (if any) outside the lock.
// 在锁之外保留新值(如果有)。
ObjcAssociation old_association(0, nil);
// acquireValue会对retain和copy进行操作,
id new_value = value ? acquireValue(value, policy) : nil;
{
// 关联对象的管理类
AssociationsManager manager;
// 获取关联的 HashMap -> 存储当前关联对象
AssociationsHashMap &associations(manager.associations());
// 对当前的对象的地址做按位去反操作 - 就是 HashMap 的key (哈希函数)
disguised_ptr_t disguised_object = DISGUISE(object);
if (new_value) {
// break any existing association.
// 获取 AssociationsHashMap 的迭代器 - (对象的) 进行遍历
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
// secondary table exists
ObjectAssociationMap *refs = i->second;
// 根据key去获取关联属性的迭代器
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
// 替换设置新值
j->second = ObjcAssociation(policy, new_value);
} else {
// 到最后了 - 直接设置新值
(*refs)[key] = ObjcAssociation(policy, new_value);
}
} else {
// create the new association (first time).
// 如果AssociationsHashMap从没有对象的关联信息表,
// 那么就创建一个map并通过传入的key把value存进去
ObjectAssociationMap *refs = new ObjectAssociationMap;
associations[disguised_object] = refs;
(*refs)[key] = ObjcAssociation(policy, new_value);
object->setHasAssociatedObjects();
}
} else {
// setting the association to nil breaks the association.
// 如果传入的value是nil,并且之前使用相同的key存储过关联对象,
// 那么就把这个关联的value移除(这也是为什么传入nil对象能够把对象的关联value移除)
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
ObjectAssociationMap *refs = i->second;
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
old_association = j->second;
refs->erase(j);
}
}
}
}
// release the old value (outside of the lock).
// 最后把之前使用传入的这个key存储的关联的value释放(OBJC_ASSOCIATION_SETTER_RETAIN策略存储的)
if (old_association.hasValue()) ReleaseValue()(old_association);
}
  • ObjcAssociation old_association(0, nil)处理传进来的值得到new_value

  • 获取到管理所有关联对象的hashmap总表的管理者AssociationsManager,然后拿到hashmap总表AssociationsHashMap

  • DISGUISE(object)对关联对象的地址进行取反操作得到哈希表对应的下标index

  • 如果new_value为空(即对属性赋值为nil)就直接找到相应的表进行删除

  • 如果new_value不为空,就拿到总表的迭代器通过拿到的下标index进行遍历查找;如果找到管理对象的关联属性哈希map表,然后再通过key去遍历取值

    • 如果取到了,就先把新值设置到key上,再将旧值释放掉
    • 如果没取到,就直接将新值设置在key

getter方法——objc_getAssociatedObject分析

1
2
3
id objc_getAssociatedObject(id object, const void *key) {
return _object_get_associative_reference(object, (void *)key);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
id _object_get_associative_reference(id object, void *key) {
id value = nil;
uintptr_t policy = OBJC_ASSOCIATION_ASSIGN;
{
// 关联对象的管理类
AssociationsManager manager;
AssociationsHashMap &associations(manager.associations());
// 生成伪装地址。处理参数 object 地址
disguised_ptr_t disguised_object = DISGUISE(object);
// 所有对象的额迭代器
AssociationsHashMap::iterator i = associations.find(disguised_object);
if (i != associations.end()) {
ObjectAssociationMap *refs = i->second;
// 内部对象的迭代器
ObjectAssociationMap::iterator j = refs->find(key);
if (j != refs->end()) {
// 找到 - 把值和策略读取出来
ObjcAssociation &entry = j->second;
value = entry.value();
policy = entry.policy();
// OBJC_ASSOCIATION_GETTER_RETAIN - 就会持有一下
if (policy & OBJC_ASSOCIATION_GETTER_RETAIN) {
objc_retain(value);
}
}
}
}
if (value && (policy & OBJC_ASSOCIATION_GETTER_AUTORELEASE)) {
objc_autorelease(value);
}
return value;
}

weak置空原理

在weak一行打下断点运行项目

1
2
3
4
5
6
7
int main(int argc, const char * argv[]) {
@autoreleasepool {
FXPerson *person = [[FXPerson alloc] init];
id __weak person = object;
}
return 0;
}

Xcode菜单栏Debug->Debug Workflow->Always show Disassembly打上勾查看汇编——汇编代码会来到libobjc库的objc_initWeak

app weak置空原理

weak创建过程

objc_initWeak

  • location:表示__weak指针的地址(我们研究的就是__weak指针指向的内容怎么置为nil

  • newObj:所引用的对象,即例子中的person

1
2
3
4
5
6
7
8
9
10
id objc_initWeak(id *location, id newObj)
{
if (!newObj) {
*location = nil;
return nil;
}

return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
(location, (objc_object*)newObj);
}

method swizzing坑点

Runloop

  • Runloop和线程是什么关系?

    • 每条线程都有唯一的一个与之对应的RunLoop对象,其关系是保存在一个全局的 Dictionary 里;主线程的RunLoop已经自动创建,子线程的RunLoop需要主动创建;RunLoop在第一次获取时创建,在线程结束时销毁
  • Runloopmode作用是什么?

    • 指定事件在运行循环中的优先级的,线程的运行需要不同的模式,去响应各种不同的事件,去处理不同情境模式。(比如可以优化tableview的时候可以设置UITrackingRunLoopMode下不进行一些操作,比如设置图片等。)
  • +scheduledTimerWithTimeInterval:的方式触发的timer,在滑动页面上的列表时,timer会暂停回调, 为什么?

    • 滑动scrollView时,主线程的RunLoop会切换到UITrackingRunLoopMode这个Mode,执行的也是UITrackingRunLoopMode下的任务(Mode中的item),而timer是添加在NSDefaultRunLoopMode下的,所以timer任务并不会执行,只有当UITrackingRunLoopMode的任务执行完毕,runloop切换到NSDefaultRunLoopMode后,才会继续执行timer
  • 如何解决在滑动页面上的列表时,timer会暂停回调?

    • Timer放到NSRunLoopCommonModes中执行即可

      • [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

      • [[NSRunLoop currentRunLoop] run];

  • NSTimer使用时需要注意什么?

    • 注意timer添加到runloop时应该设置为什么mode
    • 注意timer在不需要时,一定要调用invalidate方法使定时器失效,否则得不到释放
  • RunLoop 有哪些应用?

    • 常驻内存、AutoreleasePool 自动释放池
  • AutoreleasePoolRunLoop 有什么联系?

    iOS应用启动后会注册两个 Observer 管理和维护 AutoreleasePool。应用程序刚刚启动时默认注册了很多个Observer,其中有两个Observer的 callout 都是 _ wrapRunLoopWithAutoreleasePoolHandler,这两个是和自动释放池相关的两个监听。

    • 第一个 Observer 会监听 RunLoop 的进入,它会回调objc_autoreleasePoolPush() 向当前的 AutoreleasePoolPage 增加一个哨兵对象标志创建自动释放池。这个 Observer 的 order 是 -2147483647 优先级最高,确保发生在所有回调操作之前。

    • 第二个 Observer 会监听 RunLoop 的进入休眠和即将退出 RunLoop 两种状态,在即将进入休眠时会调用 objc_autoreleasePoolPop() 和 objc_autoreleasePoolPush() 根据情况从最新加入的对象一直往前清理直到遇到哨兵对象。而在即将退出 RunLoop 时会调用objc_autoreleasePoolPop() 释放自动自动释放池内对象。这个Observer 的 order 是 2147483647 ,优先级最低,确保发生在所有回调操作之后。

Block

多线程

内存管理

计算机系统题

数据结构&算法题

性能优化

设计模式

核心动画

SWIFT

Flutter

iOS 面试题

算法

main 函数之前做了什么

APP优化

坚持原创技术分享,您的支持将鼓励我继续创作!

欢迎关注我的其它发布渠道