【iOS】—— block,KVC,KVO,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_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了所有依赖的keysetter方法,当[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_isKeykeyisKey的顺序查找成员变量,找到直接赋值,找不到则抛出异常;
  • accessInstanceVariablesDirectly方法返回NO,则直接抛出异常;
    在这里插入图片描述

取值:KVC取值(valueForKey:)

  • 首先会按照getKeykeyisKey_key的顺序查找方法,找到直接调用取值
  • 若未找到,则查看+ (BOOL)accessInstanceVariablesDirectly的返回值,若返回NO,则直接抛出异常;
  • 若返回的YES,则按照_key_isKeykeyisKey的顺序查找成员变量,找到则取值;
  • 找不到则抛出异常;
    在这里插入图片描述

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有成员变量的效果。

猜你喜欢

转载自blog.csdn.net/m0_62386635/article/details/131795208