美文网首页
Objective-C

Objective-C

作者: AlanGe | 来源:发表于2021-07-09 17:10 被阅读0次

    一、分类

    分类就是对装饰模式的一种具体实现。它的主要作用是在不改变原有类的前提下,动态地给这个类添加一些方法。

    你用分类都做了哪些事?
    声明私有方法
    分解体积庞大的类文件
    把Framework的私有方法公开

    • 特点
      1.运行时决议
      2.可以为系统类添加分类

    • 分类中都可以添加哪些内容?
      1.实例方法
      2.类方法
      3.协议
      4.属性:

    在分类中添加了一个属性时,只是声明了对应的set方法和get方法,并没有为我们在分类当中添加了实例变量。
    如果要为分类添加实例变量,是通过关联对象来添加的。

    分类结构体:
    category_t实际上就是我们创建的分类文件

    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; // 实例属性列表
        // 如果是元类,就返回类方法列表;否则返回实例方法列表
        method_list_t *methodsForMeta(bool isMeta) {
            if (isMeta) return classMethods;
            else return instanceMethods ;
        }
        // 如果是元类,就返回 nil,因为元类没有属性;否则返回实例属性列表,但是...实例属性
        property_list_t *propertiesForMeta (bool isMeta) {
            if (isMeta) return nil; // classProperties;
            else return instanceProperties;
        }
    };
    

    这里没有实例变量的结构
    从类别的结构体我们可以看到,分类可以添加属性,不能添加成员变量

    分类加载调用栈
    1. _objc_init: 初始化runtime,进行了一些初始化操作,注册了镜像状态改变时的回调函数
    2. map_2_images:内存镜像的相关处理, 主要是加锁并调用 map_images_nolock
    3. map_images_nolock: 完成所有 class 的注册、fixup等工作,还有初始化自动释放池、初始化 side table 等工作并在函数后端调用了 _read_images
    4. _read_images: 读取镜像,加载可执行文件,比如加载类、Protocol、Category
    5. remethodizeClass: 分类的内部实现

    当我们程序启动之后,在运行时会调用_objc_init方法,实际上是在runtime的初始化方法,然后会调用一系列方法,最后加载分类。
    例如,调用_objc_init初始化方法后,会调用map_2_images方法,然后调用map_images_nolock方法,然后再调用_read_images,最后调用remethodizeClass:,分类的加载的逻辑都在remethodizeClass:方法的内部开始。

    调用runtime的_objc_init方法,进行初始化操作,注册镜像状态改变时的回调函数,调用内存镜像相关处理的map_2_images函数,map_2_images主要是加锁并调用map_images_nolock,map_images_nolock完成所有 class 的注册、fixup等工作,还有初始化自动释放池、初始化 side table 等工作并在函数后端调用读取镜像函数_read_images,读取镜像函数_read_images加载可执行文件,比如加载类、Protocol、Category,最后调用remethodizeClass函数,分类的内部实现都在remethodizeClass函数里面。,

    • 总结:
      分类添加的方法可以“覆盖”原类方法
      同名分类方法谁能生效取决于编译顺序
      名字相同的分类会引起编译报错

    • Category的优点:
      1.可以将类的实现分散到多个不同的文件或者不同的框架中,方便代码的管理;
      2.也可以对框架提供类的扩展,把不同的功能组织到不同的category里,从而按需加载想要的category。
      3.创建对私有方法的前向引用:如果其他类中的方法未实现时,或者在访问该类私有方法时编译器报错时;在类别中声明这些方法(不必提供方法实现)从而绕过编译器不会再产生警告或者错误。
      4.向对象添加非正式协议:创建一个NSObject的类别成为“创建一个非正式协议”,因为可以作为任何类的委托对象使用(声明私有方法)。
      5.apple的SDK中就大面积的使用了category这一特性。比如UIKit中的UIView。apple把不同的功能API进行了分类,这些分类包括UIViewGeometry、UIViewHierarchy、UIViewRendering等。
      6.不过除了apple推荐的使用场景,广大开发者脑洞大开,还衍生出了category的其他几个使用场景:
      6.1. 模拟多继承(另外可以模拟多继承的还有protocol)
      6.2. 把framework的私有方法公开

    • Category的局限性:
      1.category只能给某个已有的类扩充方法,不能扩充成员变量。
      2.category中也可以添加属性,但@property只会生成setter和getter的声明,不会生成实现以及成员变量。
      3.如果category中的方法和类中原有的方法同名,运行时会优先调用category中的方法。也就是,category中的方法会覆盖掉类中原有的方法。所以开发中尽量保证不要让分类中的方法和原有类中的方法名相同;避免出现这种情况的解决方案是给分类的方法名统一添加前缀。比如category_xxx。
      4.如果多个category中存在同名的方法,运行时到底调用那个方法由编译器决定,最后一个参与编译的方法会被调用。

    二、关联对象

    • 关联对象的三个主要函数
    id objc_getAssociatedObject(id object, const void *key)
    void objc_setAssociatedObject(id object, const void *key,id value, objc_AssociationPolicy policy)
    void objc_removeAssociatedObjects(id object)
    
    • 第一个函数:id objc_getAssociatedObject(id object, const void *key)
      第一个参数为从该object中获取关联对象
      第二个参数为想要获取关联对象的key;
      这个函数先根据对象地址在 AssociationsHashMap 中查找其对应的 ObjectAssociationMap 对象,如果能找到则进一步根据 key 在 ObjectAssociationMap 对象中查找这个 key 所对应的关联结构 ObjcAssociation ,如果能找到则返回 ObjcAssociation 对象的 value 值,否则返回 nil;
    • 第二个函数:void objc_setAssociatedObject(id object, const void *key,id value, objc_AssociationPolicy policy)
      参数说明:
      第一个参数为从该object中获取关联对象
      第二个参数为想要获取关联对象的key;
      第三个参数value:需要和object建立关联引用对象的value;
      第四个参数policy:关联策略,等同于给property添加关键字
      首先设置一个值value,通过key和value建立一个映射关系,然后把这个映射关系通过policy的策略关联到object这个对象上面,policy实际上是来声明或者告诉这个函数,我们这个value是以copy、asign、retain等形式来关联到数组对象上面。

    • 第三个函数:void objc_removeAssociatedObjects(id object)
      根据object对象移除它的所有相关的关联对象。

    • 通过关联对象为分类添加实例变量

    #import <Foundation/Foundation.h>
    @interface Person : NSObject
    @end
    
    #import "Person.h"
    @implementation Person
    @end
    
    #import "Person.h"
    @interface Person (MyPerson)
    @property (nonatomic, copy) NSString *name;
    @end
    
    #import "Person+MyPerson.h"
    #import <objc/runtime.h>
    
    @implementation Person (MyPerson)
    - (void)setName:(NSString *)name {
        objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    }
    - (NSString *)name {
        return objc_getAssociatedObject(self, @"name");
    }
    @end
    
    • 关联对象的本质
      关联对象由AssociationsManager管理,并在AssociationsHashMap存储。
      所有对象的关联内容都在同一个全局容器中。
    • 问题:能否为分类添加成员变量?
      能为分类添加成员变量,我们不能在分类的声明或者定义实现的时候直接为分类添加成员变量,但可以用关联对象的技术来为分类添加成员变量,来达到可以为分类添加成员变量的效果。

    • 问题:关联对象技术实现的为分类添加的成员变量,这个成员变量被放到了哪里?
      答:关联对象由AssociationsManager管理,并在associationsHashMap存储,所有对象的管理内容都在同一个全局容器中,即关联对象技术实现的为分类添加的成员变量被统一放到一个全局的容器当中,并且为不同的类添加的关联对象都保存在同一个全局容器中

    三、扩展(Extension)

    • 扩展的作用
      声明私有属性
      声明私有方法
      声明私有成员变量

    • 扩展的特点:
      编译时决议
      只以声明的形式存在,多数情况下寄生于宿主类的.m中。
      不能为系统类添加扩展。

    • 分类和扩展的区别
      1、分类是运行时决议,扩展是编译时决议;
      2、分类可以有声明有实现,而扩展只有声明,实现直接写在宿主类的 .m 中;
      3、分类可以为系统类添加分类,而扩展不能为系统添加扩展。

    四、代理

    • 代理( Delegate)
      准确的说是一种软件设计模式
      iOS当中以@protocol形式体现。
      传递方式一对一。

    • 代理的工作流程


    协议中可以定义:属性,方法

    问题:我们在协议中声明的方法或者属性,代理方都必须实现吗?
    不一定,在协议中被声明为require,是必须实现的,如果是optional的,可以不实现。

    • 代理一般声明为weak以规避循环引用


    问题:代理方和委托方之间是是以什么样的关系存在的?
    代理方用strong关键字来强持有委托方,委托方用weak关键字来声明代理方,弱引用代理方,这样的目的是以规避循环引用。

    五、通知(NSNotification)

    数据层,网络层,业务逻辑层,UI层

    • 通知的特点:
      是使用观察者模式来实现的用于跨层传递消息的机制。
      传递方式为一对多。

    通知是怎样实现一对多的传递方式的
    通知一对多的流程:


    • 代理和通知的区别:
      1、设计模式上的区别:代理是用代理模式实现的,通知是用观察者模式实现的。
      2、传递方式上的区别:代理是一对一的,通知是一对多的。

    • 通知机制


    在通知中心(NSNotificationCenter)这个系统类当中,可能内部会维护一个Notification_Map表,或者说字典,这个字典当中的key是notificationName,即监听的通知名称,值就是就是我们添加的Observers_List,对于同一个名称的通知,添加多个Observer,所以Observer对应的值,应该是一个数组列表,这个列表中的每个成员,都包含通知接收的观察者和这个观察者调用的方法,比如说,我们收到这个通知之后,观察者的回调方法是哪个,那么在这个列表当中的每个元素里面也会体现关于这个通知回调方法的一些相关数据信息。

    六、KVO

    KVO是Key-value observing的缩写。
    KVO是Objective-C对观察者设计模式的又一实现。
    Apple使用了isa 混写(isa-swizzling)来实现KVO。

    • isa混写技术:


    • kvo的实现机制和原理


    当我们调用了addObserver:forKeyPath:options:之后,系统在运行时动态创建NSKVONotifying_A这么一个子类,同时将原来的类A的isa指针指向新创建的类,重写set方法,来实现kvo的机制的。
    NSKVONotifying_A是原来的类A的一个子类,之所以做这个继承关系,是为了重写A类的setter方法,然后这个子类对setter方法的重写来达到可以通知观察者的目的。

    • 代码实现:
    子类重写了set方法的具体实现:
    重写的Setter添加的方法:
    -(void)willChangeValueForKey:(NSString *)key
    -(void)didChangeValueForKey:(NSString *)key
    
    // NSKVONotifying_А 的setter实现
    - (void)setValue:(id)obj {
        [self willChangeValueForKey:@"keyPath"];
        //调用父类实现,也即原类的实现
        [super setValue:obj];
        [self didChangeValueForKey:@"keyPath"];
    }
    

    didChangeValueForKeyceiling这个方法就会触发 observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context 回调方法来通知我们的观察者value发生了变化

    • 问题:通过kvc设置value能否生效?
      [obj setValue:@2 forKey:@"value"];
      能,调用了对应对象obj的set方法。

    • 问题:通过成员变量直接赋值value能否生效?
      [obj increase];
      不能直接触发系统的kvo的,添加willChangeValueForKey和didChangeValueForKeyceiling实现手动触发kvo。

    // 直接为成员变量赋值,手动触发kvo
    [self willChangeValueForKey:@"value"];
    _value += 1;
    [self didChangeValueForKey:@"value”];
    
    • 问题:直接对成员变量赋值是否能触发kvo?
      不能,如果要实现kvo,需要手动添加kvo才能实现,即在成员变量赋值前后分别实现willChangeValueForKey和didChangeValueForKeyceiling方法才能实现。

    • KVO总结
      使用setter方法改变值KVO才会生效。
      使用setValue:forKey:改变值KVO才会生效。
      成员变量直接修改需手动添加KVO才会生效。

    • 问题:什么是kvo?
      1)KVO是Key-value observing的缩写。
      2)kvo是观察者设计模式的一种实现,
      3)系统采用了isa混写(isa-swizzling)来实现kvo的。

    • 问题:kvo是如何通过isa混写(isa-swizzling)技术来实现kvo机制的?
      系统在运行时为我们动态创建了一个子类,然后改写isa的指向这个子类,同时重写set方法来实现kvo机制的。

    • 问题:isa混写技术在kvo中是怎样体现的?
      当我们调用了addObserver:forKeyPath:options:之后,系统在运行时动态创建NSKVONotifying_A这么一个子类,同时将原来的类A的isa指针指向新创建的类,重写set方法,来实现kvo的机制的,在调用父类的[super setValue:obj]之前加上[self willChangeValueForKey:@"keyPath”],之后加上[self didChangeValueForKey:@"keyPath”],didChangeValueForKeyceiling这个方法就会触发 observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context 回调方法来通知我们的观察者value发生了变化。

    • 问题:直接对成员变量赋值是否能触发kvo?
      不能,如果要实现kvo,需要手动添加kvo才能实现,即在成员变量赋值前后分别实现willChangeValueForKey和didChangeValueForKeyceiling方法才能实现。

    七、KVC

    KVC:是Key-value coding的缩写,键值编码技术,和键值编码技术相关的两个方法:

    -(id)valueForKey:(NSString *)key
    -(void)setValue:(id)value forKey:(NSString *)key
    valueForKey:调用某一个实例的valueForKey方法,来获取和key同名或者相似名称的实例变量的值
    setValue:forKey:设置某一对象当中和key同名或者相似名称的实例变量的值
    
    • 问题:我们通过键值编码技术,是否会破坏面向对象的编程方法,或者说是否有违背于面向对象编程思想呢?
      答:会,valueForKey和setValue中的key是没有任何限制的,也就是说,如果在我们知道一个类或者实例,它内部的某个私有成员变量名称的情况下,我们在外界可以通过已知的key来对它的私有成员变量进行设置或者访问操作的,所以从这个角度来说,KVC这种编码技术,是会破坏面向对象的编程方法的。

    • value:forKey的系统实现流程


    当调用value:forKey的时候,首先系统会判断我们通过这个key所访问的对应的实例变量是否有相应的getter方法,如果有,直接调用,然后结束value:forKey的调用流程;
    如果对应的getter方法不存在,就会通过系统的+ (BOOL)accessInstanceVariablesDirectly判断实例变量是否存在,如果和我们这个key相同或者相似的成员变量存在的话,那么直接获取这个实例变量的值,然后结束value:forKey流程;
    如果这个实例变量不存在,就会调用当前实例的valueForUndefinedKey:方法,然后会抛出NSUndefinedKeyException未定义key的异常,然后结束value:forKey调用流程。

    • 访问器方法是否存在的判断规则:
      Accessor Method
      <getKey>
      <key>
      <isKey>

    我们在用value for key 获取一个key同名或者相似名称的成员变量的时候,访问器定义方法的定义,实际上也涉及到一个相似的概念,比如说,如果我们实现了get方法,叫getKey,同时满足驼峰命名方法,那么value for key的调用流程,也会认为这个key所对应的成员变量是存在访问器方法的。最常见的属性名称,也就是我们get方法的名称。除了<getKey>和<key>,还有<isKey>,如果说,我们传递参数的key,那么和它对应的成员变量,如果实现了一个叫isKey的get方法,那么在value for key调用流程当中也会认为它的访问器方法是存在的。

    • 实例变量的说明:同名的实例变量(相似名称的成员变量)
      KVC Instance var
      _key
      _isKey
      key
      isKey
      只要存在_key、_isKey、key、isKey,就可以获取到对应的值。

    问题:实例变量是否存在的判断规则
    只要存在_key、_isKey、key、isKey,就可以获取到对应的值。

    • setValue: forKey 的调用流程


    调用setValue:forKey的时候,首先会判断是否有和这个key相关的Setter方法的存在,如果有,直接调用,然后结束setValue:forKey流程;
    如果没有,就会通过系统的+ (BOOL)accessInstanceVariablesDirectly判断实例变量是否存在,如果这个实例变量存在的话,那么对这个key所对应的成员变量进行赋值,然后结束setValue:forKey流程;
    如果这个实例变量不存在,就会调用当前实例的setValue:forUndefinedKey:方法,然后会抛出NSUndefinedKeyException未定义key的异常,然后结束setValue:forKey流程。

    八、属性关键字

    属性关键字可以分为哪几类?

    1)读写权限:readonly,readwrite(默认)
    2)原子性:
    atomic(默认):赋值和获取,是线性安全的,但对于操作是不能保证线性安全的。
    nonatomic
    3)引用计数器
    retain(MRC):修饰对象
    strong(ARC):修饰对象
    assign(修饰基本数据类型和对象类型)
    unsafe_unretained(MRC中使用比较频繁)
    weak
    copy

    • 问题:atomic是怎样保证线程安全的?
      用互斥锁来保证线程安全性。
    pthread_mutex 互斥锁使用方式:
    第一步:初始化锁属性
    第二步:初始化互斥锁,销毁锁属性
    第三步:加锁 解锁
    第四步:销毁互斥锁
    
    声明互斥锁
    pthread_mutex_t _lock; // recursive lock
    初始化互斥锁
    pthread_mutexattr_t attr;//互斥锁
    pthread_mutexattr_init (&attr);//初始化互斥锁
    pthread_mutexattr_settype (&attr, PTHREAD_MUTEX_RECURSIVE);
    pthread_mutex_init (&_lock, &attr);
    pthread_mutexattr_destroy (&attr);
    加锁解锁
    pthread_mutex_lock(&_lock);
    //do something
    pthread_mutex_unlock(&_lock);
    销毁锁
    pthread_mutex_destroy(&_lock);
    

    assign的特点

    修饰基本数据类型,如int、BOOL等。
    修饰对象类型时,不改变其引用计数。
    会产生悬垂指针。

    悬垂指针会造成内存泄露
    assign所修饰的对象,在被释放之后,assign指针仍然指向原对象内存地址,这个时候,如果通过assign指针继续访问原对象的话,可能就会由于悬垂指针的原因造成内存泄露或者程序异常。

    空指针:指针指向的地址为空的指针叫空指针(NULL指针)
    野指针:是指向“垃圾”内存(不可用内存)的指针
    产生原因:指针创建时未初始化。指针变量刚被创建时不会自动成为NULL指针,它会随机指向一个内存地址。
    悬垂指针:指针所指向的对象已经被释放或者回收了,但是指向该对象的指针没有作任何的修改,仍旧指向已经回收的内存地址。 此类指针称为垂悬指针。

    • weak的特点
      不改变被修饰对象的引用计数。
      所指对象在被释放之后会自动置为nil。

    • weak和assign的区别是
      1)weak只可以修饰对象,assign既可以修饰对象,也可以修饰基本数据类型
      2)assign修饰的对象被释放以后,assign指针仍然指向原对象的内存地址,weak修饰的对象被释放以后,weak指针指向nil

    weak和assign都不改变对象的引用计数

    • 问题:weak修饰的对象被释放以后为什么会被置为nil?
      当一个对象被dealloc之后,那么在dealloc的内部实现当中会去调用弱引用清除的相关函数 (weak_clear_no_lock()),然后在这个函数的内部实现当中会根据当前对象的指针查找弱引用表,把当前对象相对应的弱引用拿出来,是一个数组,然后遍历这个数组当中的所有弱引用指针分别置为nil。

    copy

    • 浅拷贝
      浅拷贝就是对内存地址的复制,让目标对象指针和源对象指向同一片内存空间。

    • 浅拷贝的特点:
      1、引用计数器增加。
      2、不会开辟新的内存空间,不会发生新的内存分配。

    • 深拷贝
      深拷贝让目标对象指针和源对象指针指向两片内容相同的内存空间。

    • 深拷贝的特点:
      1、不会增加被拷贝对象的引用计数
      2、深拷贝产生新的内存分配

    • 深拷贝VS浅拷贝
      是否开辟了新的内存空间:浅拷贝不会开辟新的内存空间,深拷贝会
      是否影响了引用计数:浅拷贝会使引用计时器增加,深拷贝不会

    浅拷贝的特点:
    1、引用计数器增加。
    2、不会开辟新的内存空间,不会发生新的内存分配。
    深拷贝的特点:
    1、不会增加被拷贝对象的引用计数
    2、深拷贝产生新的内存分配

    copy关键字

    • 总结
      可变对象的copy和mutableCopy都是深拷贝。
      不可变对象的copy是浅拷贝,mutableCopy是深拷贝。
      copy方法返回的都是不可变对象

    • 问题:@property(copy) NSMutableArray *array;
      这样声明一个成员属性的话,会导致什么样的问题?
      如果赋值过来的是NSMutableArray,copy之后是NSArray,如果赋值过来的是NSArray,copy之后是NSArray,由于array是NSMutableArray,如果调用方调用NSMutableArray的添加或者删除对象方法,此时由于array被сopy的结果是一个不可变的对象,那么对于不可变对象调用子类的添加或者删除对象方法,就会产生异常,引起crash。

    • 问题:MRC下如何重写retain修饰变量的setter方法?

    @property (nonatomic, retain) id obj;
    - (void)setObj:(id)obj {
        if(_obj != obj) {
            [_obj release];
            _obj = [obj retain];
        }
    }
    

    九、Block

    1、Block本质

    Block是将函数及其执行上下文封装起来的对象。
    block本质上也是一个oc对象

    Block调用即是函数的调用。

    2、Block截获变量

    • 截获变量的类型:
      局部变量:基本数据类型,对象类型
      静态局部变量
      全局变量
      静态全局变量

    • block截获变量特性:
      1.局部变量:
      1.1.基本数据类型:截获其值。
      1.2.对象类型:连同所有权修饰符一起截获。
      3.局部静态变量:以指针形式截获。
      4.全局变量、静态全局变量:不截获。

    3、__block修饰符

    • __block修饰符
      一般情况下,对被截获变量进行赋值操作的时候需要添加添加__block修饰符。
      赋值 != 使用
    NSMutableArray *array = [NSMutableArray array];
    void(^Block)(void) = ^{
        [array addObject:@123];
    };
    Block();
    
    是否需要在array的声明处添加__block修饰符?
    不需要用__block修饰array,因为是使用,不是赋值。
    
    NSMutableArray *array = nil;
    void(^Block)(void) = ^{
        array = [NSMutableArray array];
    };
    Block();
    
    是否需要在array的声明处添加__block修饰符?
    这里是赋值,所以要添加__block修饰。
    
    • 对变量进行赋值时
      1.需要__block修饰符:局部变量
      基本数据类型
      对象类型
      2.不需要__block修饰符
      静态局部变量
      全局变量
      静态全局变量
    {
        __block int multiplier = 6;
        int(l Block)(int) = int(int num){
            return num * multiplier ;
        };
        multiplier = 4;
        NSLog(@"result is %d", Block(2)); 
    }
    
    "result is 8"
    

    原因:__block修饰的变量变成了对象

    栈上的__block的__forwarding指针是指向__block自身的。

    4、Block内存管理

    impl.isa = &_NSConcreteStackBlock;(isa:标识当前block的类型)
    1、_NSConcreteGlobalBlock
    2、_NSConcreteStackBlock
    3、_NSConcreteMallocBlock

    • block的类型:
      1、全局类型的block
      2、栈上面的block
      3、堆上面的block

    不同类型的block在内存上面的分布:
    1、全局类型的block:已初始化数据区
    2、栈上面的block:栈区
    3、堆上面的block:堆区

    Block的copy操作

    Block类型 Copy结果
    _NSConcreteStackBlock
    _NSConcreteGlobalBlock 数据区 什么也不做
    _NSConcreteMallocBlock 增加引用计数
    • 问题:我们在何时需要对block进行copy操作?
      譬如我们声明一个对象成员变量是一个block,而在栈上面创建这个block,同时赋值给成员变量的这个block,如果成员变量的block没有使用copy关键字,譬如使用assign的话,那么当我们具体通过成员变量去访问对应的block的话,可能就由于栈所对应的函数退出之后,在内存当中销毁掉了,这个时候继续访问,就可能会引起内存崩溃。

    • 栈上Block的销毁


    • 栈上Block的Copy


    • 问题:当我们对栈上的block进行copy操作之后,假如在MRC环境下是否会引起内存泄漏呢?
      会。假如说我们进行copy操作之后,同时这个block或者说堆上面的这个block没有额外其他成员变量去指向它,那么和我们去alloc出一个对象,没有调用对应的release的效果是一样的,会产生内存泄漏。

    • 栈上__block变量的Copy


    __block变量中是有一个__forwarding指针,栈上的__forwarding是指向block自身的,前提是栈上的。

    • __forwarding

    我们在栈上创建了一个变量multiplier,如果通过__block修饰符修饰之后,multiplier就变成了一个对象,所以说multiplier=6 的赋值,实际上不是对变量赋值,而是通过multiplier这个对象的__forwarding指针,然后对其成员变量multiplier进行赋值,_blk实际上是某一个对象的成员变量,当对_blk进行赋值操作的时候,实际上就会对_blk进行copy操作,那么_blk就会被拷贝到堆上面去,然后我们对block进行执行,multiplier=6 代表的含义就是通过栈上的multiplier的__forwarding指针找到堆上面所对应的__block变量的copy副本,然后对其副本进行值的修改,右边block执行逻辑,我们调用了堆上面的block,入参为4,在我们调用的时候,这个时候我们在block的执行体当中所使用的multiplier __block变量,实际上使用的是堆上面的__block变量,那么我们在这里,实际上经过copy之后,multiplier=6 ,它是对堆上面的block变量的修改,所以我们在右边调用之后的结果是4和6的乘积为24。

    • __forwarding存在的意义
      不论在任何内存位置,都可以顺利的访问同一个__block变量,我们没有对__block变量进行copy的话,那么实际上操作的就是栈上的这个__block变量,如果进行了copy操作之后,无论是在栈上还是在堆上,我们对__block的修改或者赋值操作,实际上操作的都是堆上面的__block变量,同时栈上关于__block变量的使用,也都是使用堆上面的__block变量。

    5、Block循环引用

    当前对象用copy属性关键字声明了_strBlk,所以当前对象对_strBlk有一个强引用的,而_strBlk的表达式当中又使用到了_array成员变量,block截获变量的时候,对于对象类型的局部变量或者成员变量,实际上会连同属性关键字一起截获的,而array一般用strong属性关键字修饰的,所以在这个block中就有一个strong类型的指针指向当前对象,由此就产生了一个自循环引用。
    会造成自循环引用,属于自循环。

    解决方案:我们可以通过在当前栈上面创建一个__weak修饰符修饰的一个weakArray变量,来指向原对象的array成员变量,然后在block当中使用我们创建的weakArray,由此,我们就可以解除这个自循环引用。

    • 问题:为什么通过__weak属性关键字去修饰对应的成员变量可以达到避免循环引用的目的?
      由于我们的这个block对其所截获的变量如果是一个对象类型的,那么就会连同其所有权修饰符一起进行截获,如果我们在外部定义了这个变量是__weak所有权修饰符的,那么在block当中所产生的结构体里面所持有的成员变量也是__weak类型的,那么由此就可以解释为什么在block外部定义一个__weak所有权修饰的变量,就可以避免循环引用的问题。

    • 请思考,这段代码有问题吗?

    {
        __block Block * blockSelf = self;
        _blk = ^int(int num) {
            // var=2
            return num * blockSelf.var;
        };
        _blk(3);
    }
    

    在栈上面,我们通过__block修饰符修饰的变量来指向当前对象,同时当前对象的成员变量_blk在这里进行创建,block的表达式当中有使用到blockSelf的var变量。
    在MRC下,不会产生循环引用,在ARC下,会产生循环引用,引起内存泄漏

    ARC下的引用循环

    ARC下的解决方案

    {
        __block MyBlock *blockSelf = self;
        _blk = ^int(int num){
            // var=2
            int result = num * blockSelf.var;
            blockSelf = nil;
            return result;
        };
        _blk(3);
    }
    

    MRC下同样没问题

    这种解决方案有一个弊端:就是说如果我们很长一段时间或者说永远都不会调用_blk的话,那么这个引用循环的环就会一直存在。

    相关文章

      网友评论

          本文标题:Objective-C

          本文链接:https://www.haomeiwen.com/subject/ykufpltx.html