面向对象
一个NSObject对象占用多少内存
系统分配了16个字节给NSObject对象(malloc_size)
但NSObject对象内部只使用了8个字节的空间(class_getInstanceSize)
对象的isa指针指向哪里
instance对象isa指向class对象
调用对象方法时,通过isa指针找到类对象,找到对象方法实现进行调用
class对象isa指向meta-class对象
调用类方法时,通过isa指针找到元类对象,找到类方法的视线进行调用
meta-class对象isa指向基类的meta-class对象
OC的类信息存放在哪里
对象方法、属性、成员变量、协议信息,存放在class对象
类方法存放在meta-class对象
成员变量的具体值,存放在instance对象
KVO
iOS用什么方式实现一个对象的KVO(KVO的本质是什么)
利用runtime动态生成一个子类,并且让instance对象的isa指向该子类
当修改instance对象的属性时,会调用Foundation的_NSSetXXXValueAndNotify
函数
willChangeValueForKey:
父类原来的set方法
didChangeValueForKey:
内部会触发Observer的监听方法(observeValueForKeyPath:ofObject:change:context)
如何手动触发KVO
手动调用willSetValueForKey:和didChangeValueForKey:
直接修改成员变量的值会触发KVO吗
不会
通过KVC修改属性会触发KVO吗
会
KVC的赋值和取值过程?
赋值 先调用setA isA _age
Category
Category的实现原理
Category编译后的底层结构是struct category_t 里面存放分类的对象方法、类方法、属性、协议信息
在程序运行时,runtime会将Category的数据,合并到类信息中(类对象、元类对象中)
Category和Class Extension的区别是什么
Class Extension在编译时数据就在类信息中
Category是在运行时,将数据合并到类信息中
Categroy中有load方法吗?load方法是什么时候调用的?load方法能继承吗?
有load方法
load是在程序启动的时候,加载类、分类的时候调用
load方法可以继承,但一般不会主动去调用load方法,都是让系统自动调用
load、initialize方法的区别?在Category中的调用顺序,出现继承时他们之间的调用过程?
Category能否添加成员变量?如果可以,如何给Category添加成员变量?
不能直接给Category添加成员变量,但可以通过runtime实现Category有成员变量的效果
Objective-C的本质
OC的本质
底层都是C/C++代码,编译成了汇编语言、机器语言
OC的面向对象是通过C/C++的结构体实现的
将OC代码转换成C/C++代码
xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc OC源文件 -o 输出的CPP文件
一个OC对象在内存中的布局
struct NSObject_IMPL {
Class isa;
}
Class 即 objc_class
class、meta-class对象的本质结构都是struct objc_class
创建一个实例对象,至少需要多少内存
class_getInstanceSize([NSObject class]);
创建一个实例对象,实际上分配了多少内存
malloc_size(obj);
常见LLDB指令
p 打印
po 打印对象
读取内存
memory read/数据格式字节数 内存地址
x/数量格式字节数 内存地址
修改内存中的值
memory write
OC对象的分类
instance对象(实例对象)、class对象(类对象)、meta-class对象(元类对象)
class对象在内存中存储的信息主要包括
isa指针
superclass指针
属性、对象方法、协议、成员变量
meta-class对象在内存中存储的信息
isa指针
superclass指针
类方法
object_getClass([NSObject class]); 获取元类对象
class_isMetaClass([NSObject class]); 查看Class是否是元类对象
instance的isa指向class
class的isa指向meta-class
meta-class的isa指向基类的meta-class
class的superclass指向父类的class
如果没有父类,superclass指针为nil
meta-class的superclass指向父类的meta-class
基类的meta-class的superclass指向基类的class
isa指针
isa通过& ISA_MASK (位运算),得到真实地址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
31struct objc_class {
Class isa;
Class superClass;
cache_t cache; // 方法缓存
class_data_bits_t bits; // 用于获取具体的类信息
}
bits & FAST_DATA_MASK得到class_rw_t
class_rw_t {
uint32_t flags;
uint32_t version;
const class_ro_t *ro;
method_list_t *methods; // 方法列表
property_list_t *properties; // 属性列表
const protocol_list_t *protocols; // 协议列表
Class firstSubClass;
Class nextSiblingClass;
char *demangledName;
}
class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
const char *name; // 类名
method_list_t *baseMethodList;
protocol_list_t *baseProtocols;
const ivar_list_t *ivars; // 成员变量列表
const uint8_t *weakIvarLayout;
property_list_t *baseProperties;
}
KVC
setValue:forKey:原理
按照setKey: _setKey:
顺序查找方法,找到方法调用
没找到方法,查看accessInstanceVariablesDirectly方法的返回值
NO->调用setValue:forUndefinedKey: 并抛出异常NSUnknownKeyException
YES->按照_key _isKey key isKey
顺序查找成员变量,找到后直接赋值,找不到报错
valueForKey:原理
按照getKey、key、isKey、_key
顺序查找方法,找到方法调用
没找到方法,查看accessInstanceVariablesDirectly方法的返回值
NO->调用valueForUndefinedKey: 并抛出异常NSUnknownKeyException
YES->按照_key、_isKey、key、isKey
顺序查找成员变量,找到后直接取值
Caregory的底层结构1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16struct category_t {
const char *name;
classref cls;
struct method_list_t *instanceMethods;
struct method_list_t *classMethods;
struct protocol_list_t *protocols;
struct property_list_t *instanceProperties;
struct property_list_t *_classProperties;
method_list_t *methodForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
}
Category的加载处理过程
1.通过Runtime加载某个类的所有Category数据
2.把所有Category的方法、属性、协议数据,合并到一个大数组里,后面参与编译的Category数据,会在数组的前面
3.将合并后的分类数据(方法、属性、协议),插入到类原来数据的前面
+load方法
+load方法会在runtime加载类、分类时调用
每个类、分类的+load方法,在程序运行过程中只调用一次
调用顺序:
1.先调用类的+load,按照编译先后顺序调用(先编译,先调用),调用子类的+load之前会先调用父类的+load
2.再调用分类的+load,按照编译先后顺序调用(先编译,先调用)1
2
3
4
5
6
7
8
9
101._objc_Init
2.load_images
3.prepare_load_methods
schedule_class_load
add_class_to_loadable_list
add_category_to_loadable_list
4.call_load_methods
call_class_loads
call_category_loads
(*load_method)(cls, SEL_load)
+load方法时根据方法地址直接调用,并不是经过objc_msgSend函数调用
+initialize方法
+initialize方法会在类第一次接收到消息时调用
调用顺序
先调用父类的+initialize,再调用子类的+initialize
先初始化父类,在初始化子类,每个类只会初始化一次1
2
3
4
5
6
7objc_msgSend
class_getInstanceMethod
lookUpImpOrNil
lookUpImpOrForword
_class_initialize
callInitialize
objc_msgSend(cls, SEL_initialize)
+initialize和+load区别
+initialize是通过objc_msgSend进行调用
如果子类没有实现+initialize,会调用父类的+initialize(所以父类的+initialize可能会被调用多次)
如果分类实现了+initialize,就会覆盖类本身的+initialize调用
如何给分类添加成员变量?
因为分类的底层结构限制,不能添加成员变量到分类,可以通过关联对象间接实现
添加关联对象1
2void objc_setAssociatedObject(id object, const void * key,
id value, objc_AssociationPolicy policy)
获得关联对象1
id objc_getAssociatedObject(id object, const void * key)
移除所有的关联对象1
void objc_removeAssociatedObjects(id object)
关联对象的原理
关联对象并不是存储在关联对象本身内存中
关联对象存储在全局统一的一个AssociationsManager中
设置关联对象为nil,就相当于移除关联对象
block的本质
block本质上是一个OC对象,内部有个isa指针
block是封装了函数调用以及函数调用环境的OC对象
block的变量捕获
局部变量 auto 捕获到block内部 访问方式:值传递
局部变量 static 捕获到block内部 访问方式:指针传递
全局变量 不捕获 访问方式:直接访问
auto变量的捕获
block的类型
block有3种类型,可以通过调用class方法或者isa指针查看具体类型,最终都是继承自NSBlock类型1
2
3__NSGlobalBlock__ (_NSConcreteGlobalBlock) 数据区域 .data区
__NSStackBlock__ (_NSConcreteStackBlock) 栈区
__NSMallocBlock__ (_NSConcreteMallocBlock) 堆区
1 | __NSGlobalBlock__ 没有访问auto变量 |
调用copy后的结果1
2
3_NSConcreteStackBlock 程序的数据区域 复制效果:什么也不做
_NSConcreteStackBlock 栈 复制效果:从栈复制到堆
_NSConcreteMallocBlock 堆 复制效果:引用计数增加
block的copy
在ARC下,编辑器会在下列情况自动将栈上的block拷贝到堆上:
1.block作为函数返回值时
2.将block赋值给强指针时
3.block作为Cocoa API中方法名含有usingBlock的方法参数时
4.block作为GCD API的方法参数时
MRC下block的建议写法:
@property(copy, nonatomic) void (^block)(void);
ARC下block的建议写法:
@property(strong, nonatomic) void (^block)(void);
@property(copy, nonatomic) void (^block)(void);
对象类型的auto变量
当block内部访问了对象类型的auto变量时
如果block在栈上,将不会对auto变量产生强引用
如果block被拷贝到堆上
会调用block内部的copy函数
copy函数内部会调用_Block_object_assign
函数_Block_object_assign
函数会根据auto变量的修饰符做出相应操作,形成强引用或弱引用
如果block从堆上移除
会调用block内部的dispose函数
dispose函数内部会调用_Block_object_dispose
函数_Block_object_dispose
函数会自动释放引用的auto变量(release)
__block
修饰符
可以解决block内部无法修改auto变量值的问题
不能修饰全局变量、静态变量
编译器会把__block
包装成一个对象
当block在栈上时,并不会对__block
变量产生强引用
当block被copy到堆时
会调用block内部的copy函数
copy函数内部会调用_Block_object_assign
函数_Block_object_assign
函数会对__block
变量形成强引用(retain)
当block从堆中移除时
会调用block内部的dispose函数
dispose函数内部会调用_Block_object_dispose
函数_Block_object_dispose
函数会自动释放引用的__block
变量(release)
__block
的__forwarding
指针
对象类型的auto变量、__block变量
当block在栈上时,对它们都不会产生强引用
解决循环引用问题 - ARC
用__weak
、__unsafe_unretained
解决
用__block
解决(必须要调用block)
解决循环引用问题 - MRC
用__unsafe_unretained
解决
用__block
解决(ARC时会retain,MRC时不会retain)