iOS开发攻城狮的集散地程序员iOS Developer

iOS文档补完计划--NSObject

2018-08-26  本文已影响141人  kirito_song

目录


NSObject类/NSObject协议

几乎所有OC对象都可以使用NSObject的方法、因为绝大部分OC对象都继承者他。

NSObject协议

方法很多与NSObject的方法相同、只是从类方法(为了简便)变成了对象方法/属性这种形式。
并且、他也被NSProxy遵循、这点正体现了OC的多继承。比如NSObjectNSProxy对象都可以使用isKindOfClass方法。
而类对象、也可以使用对象方法(大概是因为类对象也是一种对象吧)。

所以、下文中:

  1. 对于"-"的标记、本身就是可以作用于类对象的。
  2. 而"+"、但并不代表只能用于类对象。(NSObject协议中可能声明了"-"的版本)。
    比如[NSObject hash]、[[NSObject class] hash]、[[NSObject new] hash]

类的初始化

程序运行时加载(添加到Runtime中)一个Class、或者Category时调用。
并且只会调用一次。

1. +(void)load整个类最先被调用的方法
所以、对于method swizzle这种从一开始就希望起作用的操作、需要放在这里。

2. 父类先于子类、主类优先于分类
需要注意的是如果子类没有使用+(void)load方法、父类并不会被优先调用(也就是依旧按照Compile Sources的顺序)。
由于这个规则存在、我们也不需要主动实现[super load]方法。

3. 与Compile Sources的关系
只要加入Compile Sources中、即使项目中没有人对其#import也一样会调用(毕竟是动态语言)。
默认的调用的顺序、也与Compile Sources中的顺序相同。

4. 不主动实现、就不会被调用
+ load会按照模块被存储在loadable_classes/loadable_categories结构体中。而后取出、并且通过C函数指针调用。
所以、也不会经过消息转发的过程(子类没实现、并不会调用父类)。
详情可以参考《iOS基础(九) - load和initialize的实现原理》

5. 在load方法被自动调用之前、一个类仍然可以被使用
In a custom implementation of load you can therefore safely message other unrelated classes from the same image, but any load methods implemented by those classes may not have run yet.

也就是你可以这样写、但我不知道有什么意义~

@implementation Test
+ (void)load {
    [[Test3 new]hahaha];
}

向一个类发送第一条消息前被调用、对于父类实现(注意不是父类)的调用可能不止一次。

1. 父类调用在子类之前
在本类initialize(callInitialize(cls))调用之前、如果父类没被调用过、会主动调用一次。并且父类中也如此实现、也就是会递归调用。

void _class_initialize(Class cls)
{
    assert(!cls->isMetaClass());

    Class supercls;
    bool reallyInitialize = NO;

    // Make sure super is done initializing BEFORE beginning to initialize cls.
    // See note about deadlock above.
    supercls = cls->superclass;
    if (supercls  &&  !supercls->isInitialized()) {
        _class_initialize(supercls);
    }
    ...    
    if (reallyInitialize) {
            callInitialize(cls);
    }
    ...
}

2. 如果子类未实现+ (void)initialize、则会调用一次父类
所以、在官方文档以及xcode自动补全中采用以下写法

+ (void)initialize {
  if (self == [ClassName self]) {
    // ... do the initialization ...
  }
}

3. 每个类只会被调用一次
分类如果实现、则不会调用主类、这与+ (void)load不同。
所以如果需要分别定制主类以及category、应该写在+ (void)load中。

3. 调用在+(void)load之前
毕竟+(void)load也是个消息。

4. 如果一个类没有被使用(即使被#import)、便不会被调用
但如果他自己实现了+(void)load方法、系统在调用+(void)load之前、会调用+ (void)initialize进行初始化。

loadinitialize内部都实现了加锁、是线程安全的。


创建、复制和销毁

为该对象分配地址空间

对象创建后、isa以外的实例变量都默认初始化为0

对于NSObject而言、alloc其实已经初始化完毕了。但对于其他(比如UIView)类、还需要init来进行进一步配置。

作用于alloc相同。文档上上说是由于历史原因。

对已经分配了内存空间的对象进行进一步配置。

在某些情况下、init可能会返回一个新的对象(详见《iOS架构补完计划--设计模式》中对于工厂模式的介绍)。

集alloc和init于一身

相当于调用[[Class alloc] init];、也是一种历史遗留的产物、不过还挺方便。

通过自己实现<NSCopying>协议的copyWithZone:方法返回一个不可变的副本

如果没有实现协议方法、则会崩溃。

通过自己实现< NSMutableCopying >协议的mutableCopyWithZone:方法返回一个可变的副本

需要注意这两个方法并不是<NSCopying/NSMutableCopying>那个对象方法、而是系统为类对象实现的。

二者均被标记成OBJC_ARC_UNAVAILABLE、也就是ARC下不需要(主动实现?)。

但是官方文档中指出This method exists so class objects can be used in situations where you need an object that conforms to the NSCopying protocol.

也就是说、类对象也可以被copy、并且系统帮我们进行了内部实现。
需要注意的是、类对象的copy只是单纯的返回自身而已。
但是这个机制让我们可以将类对象作为key使用。

id obj0 = [Test class];
id obj1 = [Test copy];
id obj2 = [Test mutableCopy];
id obj3 = [obj0 copyWithZone:nil];
id obj4 = [obj0 mutableCopyWithZone:nil];
NSDictionary * dic = @{obj0:@"0",obj1:@"1",obj2:@"2",obj3:@"3",obj4:@"4"};
    
NSLog(@"%p_obj0",obj0);
NSLog(@"%p_obj1",obj1);
NSLog(@"%p_obj2",obj2);
NSLog(@"%p_obj3",obj3);
NSLog(@"%p_obj4",obj4);
NSLog(@"%@_dic",dic);


//打印
NSObject[46855:3785930] category_test_initialize
NSObject[46855:3785930] 0x10235a1d0_obj0
NSObject[46855:3785930] 0x10235a1d0_obj1
NSObject[46855:3785930] 0x10235a1d0_obj2
NSObject[46855:3785930] 0x10235a1d0_obj3
NSObject[46855:3785930] 0x10235a1d0_obj4
NSObject[46855:3785930] {
    Test = 0;
}_dic
关于深拷贝和浅拷贝

深拷贝
产生新对象的情况
浅拷贝
是指未产生新对象的情况(刚才对类对象的拷贝就是典型的浅拷贝)
简而言之
只有不可变对象的copy方式,是浅复制,其他都是深复制。
更多可以查阅《iOS基础深入补完计划--带你重识Property》

当一个对象的引用计数为0时、系统就会将这个对象释放。

我们不需要、也不应该主动调用该方法。只需要处置一些不会随着实例生命周期而变化的事情即可(比如通知、C对象的free)。


类/对象的识别与判等

返回类对象

对象的 [someObj class]方法、是NSObject的协议方法

返回父类对象

通常来讲、返回对象的地址(NSObject、UIView)。
对于字符串/字典/数组、根据内容不同可能对内容有不同的hash方式、可以看看《解读Objective-C中的[NSString hash]方法》

id obj0 = [NSObject new];
id obj1 = [NSObject class];
id obj2 = [NSObject new];
id obj3 = [NSObject class];
id obj4 = [UIView new];
id obj5 = [UIView class];
id obj6 = [NSString new];
id obj7 = [NSString class];
id obj8 = [NSDictionary new];
id obj9 = [NSDictionary class];
id obj10 = [NSArray new];
id obj11 = [NSArray class];


NSLog(@"obj0::%zd_%ld",[obj0 hash],(NSUInteger)obj0);
NSLog(@"obj1::%zd_%ld",[obj1 hash],(NSUInteger)obj1);
NSLog(@"obj2::%zd_%ld",[obj2 hash],(NSUInteger)obj2);
NSLog(@"obj3::%zd_%ld",[obj3 hash],(NSUInteger)obj3);
NSLog(@"obj4::%zd_%ld",[obj4 hash],(NSUInteger)obj4);
NSLog(@"obj5::%zd_%ld",[obj5 hash],(NSUInteger)obj5);
NSLog(@"obj6::%zd_%ld",[obj6 hash],(NSUInteger)obj6);
NSLog(@"obj7::%zd_%ld",[obj7 hash],(NSUInteger)obj7);
NSLog(@"obj8::%zd_%ld",[obj8 hash],(NSUInteger)obj8);
NSLog(@"obj9::%zd_%ld",[obj9 hash],(NSUInteger)obj9);
NSLog(@"obj10::%zd_%ld",[obj10 hash],(NSUInteger)obj10);
NSLog(@"obj11::%zd_%ld",[obj11 hash],(NSUInteger)obj11);


//打印结果
obj0::105827994210384_105827994210384
obj1::4533444264_4533444264
obj2::105827994210416_105827994210416
obj3::4533444264_4533444264
obj4::140577323666816_140577323666816
obj5::4563397296_4563397296
obj6::0_4523287328
obj7::4523970768_4523970768
obj8::0_105553116300640
obj9::4539240872_4539240872
obj10::0_105553116300656
obj11::4539240232_4539240232

hash方法只在对象被添加至NSSet和设置为NSDictionary的key时会调用

此时他会作为key的查找以及判等依据避免重复添加

为了优化判等的效率, 基于hash的NSSet和NSDictionary在判断成员是否相等时, 会这样做

Step 1: 集成成员的hash值是否和目标hash值相等, 如果相同进入Step 2, 如果不等, 直接判断不相等

Step 2: hash值相同(即Step 1)的情况下, 再进行对象判等, 作为判等的结果

也就是说。我们如果在插入对象之后手动修改了hash值、在进行查找的时候是查找不到滴。

自定义hash插入NSSet/NSDictionay

由于hash只返回对象地址、我们可以通过对象内容进行自定义hash。(特指你希望相同名字和生日不想重复插入这种情况

- (NSUInteger)hash {
    return [self.name hash] ^ [self.birthday hash];
}

判断两个对象内容是否相等、并不只是单纯判断是否为同一个对象(内存地址)。

自定义对象需要自己实现判等逻辑。

判断对象是否继承NSProxy

需要注意我们绝大部分的类都继承与NSObject而非NSProxy
所以绝大部分都会返回No。你可以自己做一个继承于NSProxy的类来测试。

判断对象是否是指定类或其子类

具体比较的、应该是对象的isa指针。可以看下面的例子:

BOOL a = [NSString isKindOfClass:[NSString class]];
BOOL b = [NSString isKindOfClass:object_getClass([NSString class])];
BOOL c = [UIView isKindOfClass:[UIView class]];
BOOL d = [UIView isKindOfClass:object_getClass([UIView class])];
NSLog(@"a::%d",a);
NSLog(@"b::%d",b);
NSLog(@"c::%d",c);
NSLog(@"d::%d",d);

//打印结果
test[4039:505795] a::0
test[4039:505795] b::1
test[4039:505795] c::0
test[4039:505795] d::1

判断对象是否是给定类的实例(注意不包含子类)

所比较的、依旧是isa指针。可以自己照上面试试。

需要注意的是:
对于NSString/NSDictionay/NSArray这类类族对象来说。直接用抽象产品(NSString/NSDictionay/NSArray)进行判等、是会失败的。

对象的 [someObj superclass]方法、是NSObject的协议方法

查看一个类对象是否是另一个类对象的子类或者本身

BOOL a = [Test isSubclassOfClass:[Test2 class]];
BOOL b = [Test2 isSubclassOfClass:[Test class]];
BOOL c = [Test isSubclassOfClass:[Test class]];
NSLog(@"a==%d,b==%d,c==%d",a,b,c);

//打印
a==1,b==0,c==1

对对象而言、并没有能直接比较从属关系的方法。


类/对象的测试

判断对象是否能够调用给定的(对象)方法。(如果用类对象来测试、自然测试的就是类方法咯)

需要注意如果只做了声明但没有实现、也是会返回No的。

[类对象]测试(类)方法是否被实现

需要注意如果只做了声明但没有实现、也是会返回No的。

所以说respondsToSelector既可以测类方法也可以测实例方法。
instancesRespondToSelector则可以用类对象来测试类方法。

测试一个类是否遵循了某个协议

主要注意:1、遵循不代表实现。2、遵循不代表必须在.h中声明。


获取方法信息

分别返回类/对象的某个对象方法以及类方法的实现(IMP)


类/对象的描述

控制台中打印的信息、就是通过这个方法输出。

类方法只打印出了类名、实例方法(NSObject协议)可能会打印出更多内容。

NSLog、就是通过这个方法输出。

类方法只打印出了类名、实例方法(NSObject协议)可能会打印出更多内容。


发送消息

在延迟之后在当前线程上调用某对象的方法。

在延迟之后使用指定的Runloop模式当前线程上调用某对象的方法。

使用在主线程上调用某对象的方法

使用指定的Runloop模式主线程上调用某对象的方法。

指定线程上调用某对象的方法

使用指定的Runloop模式在指定的线程上调用某对象的方法

在新的后台线程上调用某对象的方法

取消执行某对象先前注册的所有请求。

取消执行某对象先前注册的指定selector请求。

object参数必须与注册时相同(并不需要是同一个、但内部会经过isEqual判断)。

需要注意的是

1. 关于RunloopMode
如果没有声明指定的Runloop模式、那么就会使用默认NSDefaultRunLoopMode
如果(正式发送消息时)当前Runloop模式不匹配、则会等待直到Runloop切换到对应模式。
2. 关于wait参数:
一个布尔值,指定当前线程是否阻塞,直到在主线程上的接收器上执行指定的选择器之后。指定YES阻止此线程; 否则,指定NO立即返回此方法。
如果当前线程也是主线程,并且您YES为此参数指定,则会立即传递和处理消息。
3. 关于取消
只有在使用afterDelay参数的方法上才可以工作


动态解析(消息转发)

如果runtime调用了一个未实现的方法、在崩溃(unrecognized selector)之前会经过一下四步。

+ resolveClassMethod:
+ resolveInstanceMethod:

允许尝试解决这个问题、无论返回YES/NO。(这个我查了很久也没能找到返回值到底有什么用)

比如用runtime、为当前类添加这个方法实现。举一个官方文档的例子:


void dynamicMethodIMP(id self, SEL _cmd)
{
    // implementation ....
}

+ (BOOL) resolveInstanceMethod:(SEL)aSEL
{
    if (aSEL == @selector(resolveThisMethodDynamically))
    {
          class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
          return YES;
    }
    return [super resolveInstanceMethod:aSel];
}

官方文档中还有这样一句话:
This method is called before the Objective-C forwarding mechanism is invoked. If respondsToSelector: or instancesRespondToSelector: is invoked, the dynamic method resolver is given the opportunity to provide an IMP for the given selector first.
也就是说、这个方法会在启用消息转发前小调用。并且动态添加的方法可以被respondsToSelector/instancesRespondToSelector识别。

- forwardingTargetForSelector

允许我们为消息指定一个新的对象进行响应。

如果在前一步你没能对问题进行解决、runtime允许你将这个消息转发给一个特定的类。

从文档规范上来讲、你需要这样实现:

  1. 返回一个非nil以及非self的对象。
  2. 不知道返回啥应该返回super调用(或者干脆别实现了)
  3. 如果你指向做单纯的消息转发、用这个。反之如果想要做更高级的事(比如修改参数等等)、这个方法做不到。应该用下面的。

- methodSignatureForSelector
+ instanceMethodSignatureForSelector

正常情况下:通过SEL获取某个类/对象的对应方法签名

在消息转发(决议)的阶段:如果返回一个函数签名,系统就会创建一个NSInvocation对象并调用(下一步)-forwardInvocation:方法

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    NSMethodSignature *methodSignature = [super methodSignatureForSelector:aSelector];
    if (!methodSignature) {
        methodSignature = [NSMethodSignature signatureWithObjCTypes:"v@:*"];
    }
    return methodSignature;
}

- forwardInvocation:

允许对方法签名进行转发(并由该对象尝试执行)

- (void)forwardInvocation:(NSInvocation *)anInvocation {
    Test2 *test2 = [Test2 new];
    if ([test2 respondsToSelector:anInvocation.selector]) {

//        NSString * str;
//        [anInvocation getArgument:&str atIndex:2];
        NSString * str = @"5";
        [anInvocation setArgument:&str atIndex:2];
        
        [anInvocation invokeWithTarget:test2];
        //需要注意的是这里的invaction是不需要、也不能调用invoke执行的。否则会执行两次
//        [anInvocation invoke];

    }else {
        [super forwardInvocation:anInvocation];
    }
}
  1. 这里新的Target对象会尝试响应该方法。
  2. 如果新的Target对象依旧未实现该方法、会由该对象继续进行决议(也允许继续转发)。
  3. 参数在methodSignatureForSelector返回签名之后已经自动设置好了。我们只需要指定新的Target便可。
  4. 签名的返回值将会发回给原调用方。

- doesNotRecognizeSelector:

处理接收方无法识别的消息

这个方法必须要调用父类实现、不推荐(允许)颠覆。否则将不会抛出错误信息。

- (void)doesNotRecognizeSelector:(SEL)aSelector {
    //弹窗啊、打点啊、等等等等
    [super doesNotRecognizeSelector:aSelector];
}

官方提供了一写应用举例。当你不允许别人使用某个方法:

- (id)copy/init
{
    [self doesNotRecognizeSelector:_cmd];
}
需要注意的是

除非你从resolveInstanceMethod/resolveClassMethod阶段就用runtime添加了方法。不然每一次调用该方法都需要重新走一次消息转发的过程。


Weak相关

允许弱引用标量、对于所有allowsWeakReference方法返回NO的类都绝对不能使用__weak修饰符。否则会崩溃。

保留弱引用变量、在使用__weak修饰符的变量时、当被赋值对象的retainWeakReference方法返回NO的情况下、该变量将使用“nil” 。


最后

本文主要是自己的学习与总结。如果文内存在纰漏、万望留言斧正。如果愿意补充以及不吝赐教小弟会更加感激。


参考资料

官方文档 - NSObject_Class
NSObject Class 浅析
iOS类方法load和initialize详解
ios开发 之 NSObject详解
iOS基础(九) - load和initialize的实现原理
NSObject之一
深入理解Objective-C的Runtime机制

上一篇下一篇

猜你喜欢

热点阅读