文章目录
- block
- 1.block的原理是怎样的?本质是什么?
- 2.__block的作用是什么?有什么使用注意点?
- 3.block的属性修饰词为什么是copy?使用block有哪些使用注意?
- 4.block在修改NSMutableArray,需不需要添加__block?
- 5.关于block对不同种变量的捕获问题
- 局部变量:
- 全局变量
- 静态全局变量
- 静态局部变量
- 6.block捕获变量的原理是什么?
- 7.__block修饰符的作用
- KVO
- 1.iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)
- 总结
- 2.如何手动触发KVO?
- 3.直接修改成员变量会触发KVO么?
- KVC
- 1.通过KVC修改属性会触发KVO么?
- 2.KVC的赋值和取值的过程是怎样的?原理是什么?
- 赋值:KVC赋值(setValue:forKey:)
- 取值:KVC取值(valueForKey:)
- Category
- 1.Category的使用场合是什么?
- 2.Category的实现原理
- 3.Category和Class Extension的区别是什么?
- 4.Category中有load方法吗?load方法是什么时候调用的?load方法继承吗?
- 5.load、initialize方法的区别是什么?它们在category中的调用顺序是什么?以及出现继承时他们之间的调用过程?
- 区别:
- 在Category中的调用顺序:
- 继承时它们之间的调用过程:
- 6.Category能否添加成员变量?如果可以,如何给Category添加成员变量?
block
1.block的原理是怎样的?本质是什么?
在iOS中,Block
是一种特殊的对象。封装了函数调用以及调用环境的OC对象。用于封装代码块。它可以作为参数传递给方法或函数,并且可以在稍后的时间点执行。
Block
的本质是一个封装了一段代码以及其访问的变量的结构体。当定义一个Block
时,它会捕获其所在作用域中的变量,并将这些变量的值复制到自己的内部结构中。这样,在Block
执行时,即使变量已经超出了其作用域,仍然可以访问并使用这些变量的值。
block
转成C++的源码:
//经过clang转换后的C++代码
struct __block_impl {
void *isa;//指向所属类的指针
int Flags;//标志性参数,暂时没用到所以默认为0
int Reserved;//今后版本升级所需的区域大小。
void *FuncPtr;//函数指针,指向实际执行的函数,也就是block中花括号里面的代码内容。
};
struct __main_block_impl_0 {
struct __block_impl impl;//上面点1中的结构体的变量
struct __main_block_desc_0* Desc;//上面点2中的结构体的指针
__main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself {
printf("Block\n");
}
static struct __main_block_desc_0 {
size_t reserved; //今后版本升级所需区域的大小(一般填0)
size_t Block_size; //Block的大小
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, const char * argv[]) {
void (*blk)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA));
((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
return 0;
}
关于block的具体细节可以看看之前的博客:【iOS】—— 浅看block源码
【iOS】—— 初识block
2.__block的作用是什么?有什么使用注意点?
在iOS中,__block
是一个修饰符,用于在Block内部修改外部变量的值。它的作用是将外部变量在Block内部转为可修改的变量。
使用__block
修符可以解决Block内部无法修改外部变的问题。默认情况下,Block内部能访问外部变量的,而不能修改它们。是,当你在外部量前面加上__block
修饰符时,Block就可以修改这变量的值了。
以下是一些使用注意点:
__block
修饰符只能于局部变量,不能用全局变量或静态变量。- 在Block部使用
__block
修的变量时,需要注意循环引用的问题如果Block被强引用并且同时引用了__block
修饰的变,可能会导致环引用,造成内存漏。为了避免这种情况,可以在Block内部使用weakSelf
来弱引用self,或者使用__weak
饰符来修饰__block
变量。 - 当
__block
修饰的变量Block内部被修改,外部变量的值会被修改。这意味在Block执行完后,外部变量的值将保持被修改后的状态。 - 在使用ARC(动引用计数)的情况下,
__block
饰符会自动处理内存管理。但是,在非ARC环境下,你需要手动处理__block
变量的内存管理,确保Block执行完毕后释放它们。
3.block的属性修饰词为什么是copy?使用block有哪些使用注意?
- block一旦没有进行copy操作,就不会在堆上。MRC 下 block 如果没有 copy 到堆上,值捕获不会对外部变量引用。 虽然 ARC 环境 strong 也可以修饰 Block,那是因为编译器会对 strong 修饰的 block 也会进行一次 copy 操作。
- 因为Block的内存地址显示在栈区,栈区的特点就是创建的对象随时销毁,一旦销毁后续再次调用空对象就会造成程序崩溃。对Block进行copy操作之后,block存在堆区,所以在使用Block属性的时候Copy修饰。
- 使用注意:循环引用问题
循环引用及强弱共舞可以看看之前的博客,上面有链接
4.block在修改NSMutableArray,需不需要添加__block?
不需要,给NSMutableArray
添加元素时。__block修饰符主要用于在Block内部问和修改外部变量,以决Block内部对外部变量的捕获问题。而在修改NSMutableArray
这样可变对象时,并不需要使用__block修饰符。因为NSMutableArray
是一个指针类型的对象,当你在Block内部修改它,实际上是修改了指向该对象的指针,而是直接修改指针所指的对象本身。因此,无需使用__block修饰符来解决捕获问题。
5.关于block对不同种变量的捕获问题
先说结论:
- 全局变量: 不捕获
- 局部变量: 捕获值
- 静态全局变量: 不捕获
- 静态局部变量: 捕获指针
- const修饰的局部常量:捕获值
- const修饰的静态局部常量:捕获指针
局部变量:
int dmy = 256;
int val = 10;
const char *fmt = "val = %d\n";
void (^blk)(void) = ^{
printf(fmt, val);
};
val = 2;
fmt = "These values were changed. val = %d\n";
blk();
输出结果:
全局变量
void (^blk)(void) = ^{
printf("%d\n", quanju);
};
quanju = 60;
blk();
输出结果:
静态全局变量
void (^blk)(void) = ^{
printf("%d\n", jingquanju);
};
jingquanju = 60;
blk();
静态局部变量
int jingjubu = 10;
void (^blk)(void) = ^{
printf("%d\n", jingjubu);
};
jingjubu = 60;
blk();
6.block捕获变量的原理是什么?
- 在执行Block语法的时候,Block语法表达式所使用的自动变量的值是被保存进了Block的结构体实例中,也就是Block自身中。
- 这里值得说明的一点是,如果Block外面还有很多自动变量,静态变量,等等,这些变量在Block里面并不会被使用到。那么这些变量并不会被Block捕获进来,也就是说并不会在构造函数里面传入它们的值。
- Block捕获外部变量仅仅只捕获Block闭包里面会用到的值,其他用不到的值,它并不会去捕获。
7.__block修饰符的作用
编译器会将__block变量包装成一个对象,变成对象后就可以根据指针地址在block内部去修改外部的变量,block通过__forwarding指针去修改变量的值
- __block修饰对象类型的变量生成的结构体内部多了
__Block_byref_id_object_copy
和__Block_byref_id_object_dispose
两个函数,用来对对象类型的变量进行内存管理的操作。 __main_block_copy_0
函数中会根据变量是强弱指针及有没有被__block修饰做出不同的处理,强指针在block内部产生强引用,弱指针在block内部产生弱引用。- 当修改__block修饰的变量时,是根据变量生成的结构体这里是
__Block_byref_age_0
找到其中__forwarding
指针,__forwarding
指针指向的是结构体自己因此可以找到变量进行修改。 - 当block在栈中时,
__Block_byref_age_0
结构体内的__forwarding
指针指向结构体自己。 - 而当block被复制到堆中时,栈中的
__Block_byref_age_0
结构体也会被复制到堆中一份,而此时栈中的__Block_byref_age_0
结构体中的__forwarding
指针指向的就是堆中的__Block_byref_age_0
结构体,堆中__Block_byref_age_0
结构体内的__forwarding
指针依然指向自己。
KVO
详解KVO的博客:【iOS】—— KVO再学习
1.iOS用什么方式实现对一个对象的KVO?(KVO的本质是什么?)
- 利用
RuntimeAPI
动态生成一个子类,并且让instance对象
的isa指向这个全新的子类 - 当修改instance对象的属性时,会调用Foundation的
_NSSetXXXValueAndNotify
函数 - 接着调用父类原来的setter方法;
- 最后调用
didChangeValueForKey
,其内部会触发监听器(Oberser)的监听方法(observerValueForKeyPath:ofObject:change:context:
);
总结
- 主要用了
isa-swizzling
,修改了观察者的类信息,并且hooksetter
方法,当setter
方法调用时发送消息给所有观察者 - 由上面源码可以看出对观察者、被观察者的引用都是Not Retain, 所以对象释放前一定要移除观察者。
- 消息的发送主要由
[self willChangeValueForKey: key]
,[self didChangeValueForKey: key]
触发,并且必须成对出现,automaticallyNotifiesObserversForKey
方法用来控制,是否要主要添加上述的两个方法,默认返回值为YES,如果返回NO则不会自动添加,也就是说setter的调用以及KVC修改都不会触发通知 + (NSSet<NSString *> *)keyPathsForValuesAffectingValueForKey:(NSString *)key
此方法用来设置依赖关系,有时候需要某属性值随着同一对象的其他属性的改变而改变。可以通过事先将这样的依赖关系在类中注册,那么即便属性值间接地发生了改变,也会发送通知消息,被观察者类重写返回和key依赖的所有key集合
内部实现也比较简单,将所有依赖关系存储在全局的dependentKeyTable
中,然后hook了所有依赖的key
的setter
方法,当[self willChangeValueForKey: key]
,[self didChangeValueForKey: key]
调用时会查找所有的依赖关系,然后发送消息- KVO内部多次用到了KVC
-
- 重写
setValue:forKey
- 重写
-
- 使用
valueForKey --- valueForKeyPath
获取属性的值,尤其是在使用点语法的时候,只有valueForKeyPath
可以获得深层次的属性值。
所以KVO是基于KVC而实现的。
- 使用
2.如何手动触发KVO?
- 手动调用
willChangeValueForKey:
和didChangeValueForKey:
3.直接修改成员变量会触发KVO么?
- 不会触发KVO,KVO的本质是替换了setter方法的实现,所以只有通过set方法修改才会触发KVO。
KVC
详解KVC的博客:【iOS】—— KVC再学习
1.通过KVC修改属性会触发KVO么?
- 会触发KVO。KVC在修改属性时,会调用
willChangeValueForKey:
和didChangeValueForKey:
方法;
2.KVC的赋值和取值的过程是怎样的?原理是什么?
赋值:KVC赋值(setValue:forKey:)
- 首先会按照
setKey
、_setKey
的顺序查找方法,若找到方法,则直接调用方法并赋值; - 未找到方法,则调用
+ (BOOL)accessInstanceVariablesDirectly;
- 若
accessInstanceVariablesDirectly
方法返回YES,则按照_key
、_isKey
、key
、isKey
的顺序查找成员变量,找到直接赋值,找不到则抛出异常; - 若
accessInstanceVariablesDirectly
方法返回NO,则直接抛出异常;
取值:KVC取值(valueForKey:)
- 首先会按照
getKey
、key
、isKey
、_key
的顺序查找方法,找到直接调用取值 - 若未找到,则查看
+ (BOOL)accessInstanceVariablesDirectly
的返回值,若返回NO,则直接抛出异常; - 若返回的YES,则按照
_key
、_isKey
、key
、isKey
的顺序查找成员变量,找到则取值; - 找不到则抛出异常;
Category
1.Category的使用场合是什么?
Category
除了用来给类进行扩展外,还有一种比较高级的用法,就是用来拆分模块,将一个大的模块拆分成多个小的模块,方便进行维护和管理。什么意思呢?我就举一个很多开发人员都会存在的问题,就是AppDelegate
这个类。这个类是刚创建项目时自动生成的,用来管理程序生命周期的。在刚创建项目时,这个类中是没有多少代码的,但是随着项目的进行,越来越多的代码会被放在这个类里面。比如说集成极光推送、友盟、百度地图、微信SDK等各种第三方框架时,这些第三方框架的初始化工作,甚至是相关的业务逻辑代码都会放在这个类里面,这就导致随着APP的功能越来越复杂,AppDelegate
中的代码就会越来越多,有的甚至有几千行,看着就让人头皮发麻。
这时我们就可以利用Category
来对AppDelegate
进行拆分,首先我们就需要对AppDelegate
中的代码进行划分,把同一种功能的代码抽取出来放在一个分类里面。比如说我可以新建一个极光推送的分类,然后把所有和极光推送有关的代码都抽出来放入这个分类,把所有和微信相关的代码抽出来放进微信的分类中,后面又有新的功要添加的话我只需要新建分类就好了。维护的时候要改什么功能的代码就直接找相应的分类就好了。
2.Category的实现原理
// 定义在objc-runtime-new.h文件中
struct category_t {
const char *name; // 比如给Student添加分类,name就是Student的类名
classref_t 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; // 分类的类属性列表
};
- Category编译之后的底层结构是结构体
struct category_t
,里面存储着分类的对象方法、类方法、属性、协议信息。 - 程序运行的时候,
Runtime
会将Category
的信息合并到类信息中(class
类对象、mate-class
元类对象),后合并的分类数据会插入到原来数据的前面; - 分类没有自己的
class
对象、和mate-class
对象,因为一个类只有一个class
对象、mate-class
对象。
3.Category和Class Extension的区别是什么?
Class Extension
是在编译的时候,它的数据都已经包含在类信息中了;Category
是在运行的时候,才将数据合并到类信息中。- 分类原则上只能增加方法,但是也可以通过关联属性增加属性
- 拓展可以增加方法和属性,都是私有的。
- 扩展只能在自身类中使用,而不是子类或者其他地方。
- 类扩展是在编译阶段添加到类中,而分类是在运行时添加到类中
4.Category中有load方法吗?load方法是什么时候调用的?load方法继承吗?
- 有
+load
方法。 +load
方法会在Runtime
加载类、分类的时候调用;- 每个类的
+load
方法只会调用一次; - 先调用类的
+load
方法,(按照编译顺序,先编译,先调用),调用子类的+load
方法之前,会先调用父类的+load方法; - 再调用分类的
+load
方法,(按照编译顺序,先编译,先调用)。 - 这里
+load
方法调用的顺序比较特别,没有先调用分类的+load
方法,因为+load
方法的调用机制不是objec_msgSend
的方式,+load
是直接找到方法地址进行调用的。 load
方法能继承,不过一般不会手动调用load
方法,都是系统自动调用。
5.load、initialize方法的区别是什么?它们在category中的调用顺序是什么?以及出现继承时他们之间的调用过程?
区别:
- 调用时机
load
,当runtime加载类、分类时会调用。load方法总是在main函数之前调用,每个类、分类的load在运行时只调用一次
initialize
,在类第一次接收到消息时调用(先初始化父类,再初始化子类,每一个类只会被初始化一次) - 调用顺序
load方法
:先调用类的load,子类调用load方法之前会先调用父类的load,先编译的先调用;再调用分类的load方法,先编译的先调用
initialize方法
:先调用父类的initialize再调用当前类的initialize,如果子类没有实现initialize,则会调用父类的initialize;如果有分类,则调用最后编译的分类的initialize,就不调用本类的initialize了 - 调用本质
load
,根据IMP地址直接调用(*load_method)(cls, SEL_load)
initialize
,通过objc_msgSend进行调用 - 使用场景
在load方法中实现方法交换(Method Swizzle)
一般用于初始化全局变量或静态变量 - 相同点
两个方法会被自动调用,不需要手动调用他们 - 区别
load
是通过直接函数地址调用,只会调用一次
initialize
是通过msgSend调用 -
- 如果子类没有实现
initialize
,会调用父类的initialzie
(所以父类的initialize会被调用很多次)
- 如果子类没有实现
-
- 分类如果实现了
initialize
,就会覆盖类本身的initailize
- 分类如果实现了
在Category中的调用顺序:
- 如果有多个Category对同一个类实现了+load方法,它们的调用顺序是不确定的。编译器会将所有的+load方法放入一个全局的链表中,然后在运行时按照链的顺序依次调用。
- 对于+initialize方法,如果一个类本身实现了+initialize方法,那么它会覆盖父类的+initialize方法。如果有多个Category对同一个类实现了+initialize方法,么只有最后一个被加载的Category的+initialize方法被调用。
继承时它们之间的调用过程:
- 当一个类继承时,子类会继承父类的+load方法。父类的+load方法会在子的+load方法之前被调用。
- 对于+initialize方法,子类如果没有实现自己的+initialize方法,继承父类的+initialize方法。父类的+initialize方法会在子类的+initialize方法之前被调用如果子类实现了自的+initialize方法,么父类的+initialize方法不被调用。
6.Category能否添加成员变量?如果可以,如何给Category添加成员变量?
不能直接给Category添加成员变量,但是可以间接实现Catecory有成员变量的效果。