Objective-C

2021-07-09  本文已影响0人  AlanGe

一、分类

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

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

在分类中添加了一个属性时,只是声明了对应的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函数里面。,

二、关联对象

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)
#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

三、扩展(Extension)

四、代理

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

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

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

五、通知(NSNotification)

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

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


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

六、KVO

KVO是Key-value observing的缩写。
KVO是Objective-C对观察者设计模式的又一实现。
Apple使用了isa 混写(isa-swizzling)来实现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发生了变化

// 直接为成员变量赋值,手动触发kvo
[self willChangeValueForKey:@"value"];
_value += 1;
[self didChangeValueForKey:@"value”];

七、KVC

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

-(id)valueForKey:(NSString *)key
-(void)setValue:(id)value forKey:(NSString *)key
valueForKey:调用某一个实例的valueForKey方法,来获取和key同名或者相似名称的实例变量的值
setValue:forKey:设置某一对象当中和key同名或者相似名称的实例变量的值

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

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

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

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

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和assign都不改变对象的引用计数

copy

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

copy关键字

@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截获变量

3、__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修饰。
{
    __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的copy操作

Block类型 Copy结果
_NSConcreteStackBlock
_NSConcreteGlobalBlock 数据区 什么也不做
_NSConcreteMallocBlock 增加引用计数

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

我们在栈上创建了一个变量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。

5、Block循环引用

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

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

{
    __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的话,那么这个引用循环的环就会一直存在。

上一篇下一篇

猜你喜欢

热点阅读