OC语法

OC语法

OC语法

面向对象

一个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
31
struct 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
16
struct 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
10
1._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
7
objc_msgSend
class_getInstanceMethod
lookUpImpOrNil
lookUpImpOrForword
_class_initialize
callInitialize
objc_msgSend(cls, SEL_initialize)

+initialize和+load区别
+initialize是通过objc_msgSend进行调用
如果子类没有实现+initialize,会调用父类的+initialize(所以父类的+initialize可能会被调用多次)
如果分类实现了+initialize,就会覆盖类本身的+initialize调用

如何给分类添加成员变量?
因为分类的底层结构限制,不能添加成员变量到分类,可以通过关联对象间接实现

添加关联对象

1
2
void 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
2
3
__NSGlobalBlock__  没有访问auto变量
__NSStackBlock__ 访问了auto变量
__NSMallocBlock__ __NSStackBlock__调用了copy

调用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)

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×