Objective-C对象解析

96
MA806P
2017.07.21 23:04* 字数 1907

简介

OC是C语言的超集,是扩充C的面向对象编程语言。OC的语法基本上是照搬C语言的,对象所占内存总是分配在“堆空间”中,而绝不会分配在“栈”上。
每个OC对象实例都是指向某块内存数据的指针,所以声明变量时,类型后面要跟一个 * 字符,有时会遇到定义里不含 * 的变量它们可能会使用栈空间,这些变量保存的不是OC对象,比如CGRect是c的结构体。

例如: NSString * pointerVar = @“aa”;
pointerVar存放内存地址的变量,NSString自身的数据就存于那个地址中。可以说,变量指向NSString实例。
特殊类型id,能代指任意OC对象类型。id本身已经是指针。可以写为 id pointerVar = @“aa”; 区别在于,如果声明指定了具体类型,那么在该类实例上调用其所没有的方法时,编译器会探知此情况,并发出警告信息。

OC对象底层实现

了解了OC对象之后,来看看OC的对象是怎样通过C语言来进行实现的。由于水平有限,只能简单梳理一下,有兴趣的可查看苹果源码https://opensource.apple.com/source/objc4,进行深入了解。

OC 对象都是 C 语言结构体

描述OC对象所用的数据结构定义在运行期程序库的头文件里,每个对象结构体的首个成员是Class类的变量,该变量定义了对象所属的类,通常称为 is a 指针。

@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;
}
typedef struct objc_class *Class;
typedef struct objc_object *id;
struct objc_class : objc_object {
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    ...
};

struct objc_object {    
    isa_t isa;
    ...
};

union isa_t 
{
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;
    ...
};
    

1、结构体objc_class 中存放类的元数据。objc_class 结构体是继承自 objc_object 的,首个变量也是isa指针,说明Class本身也是OC对象。

2、superclass定义了本类的超类。类所属的类型是另外一个类,叫做“元类” (metaclass),用来表述类对象本身所具备的元数据。类方法就是定义于此处,因为这些方法可以理解成类对象的实例方法。每个类仅有一个类对象,每个类对象仅有一个与之相关的元类。如下图:


3、superclass确立了继承关系,isa指针描述了实例所属的类。通过图上的关系可一步步查询出对象是否能响应某个方法,是否遵循某个协议等。当实例方法被调用时,它要通过自己持有的 isa 来查找对应的类,然后在结构体 class_data_bits_t 中查找对应方法的实现。

4、cache:用于缓存最近使用的方法。一个接收者对象接收到一个消息时,它会根据isa指针去查找能够响应这个消息的对象。在实际使用中,这个对象只有一部分方法是常用的,很多方法其实很少用或者根本用不上。这种情况下,如果每次消息来时,我们都是methodLists中遍历一遍,性能势必很差。这时,cache就派上用场了。在我们每次调用过一个方法后,这个方法就会被缓存到cache列表中,下次调用的时候runtime就会优先去cache中查找,如果cache没有,才去methodLists中查找方法。这样,对于那些经常用到的方法的调用,但提高了调用的效率。

5、结构体 isa_t,这个结构体中包含了当前对象指向的类的信息。

OC对象实例

举个例子,当我们定义一个类,带有两个成员变量:

@interface SomeObject : NSObject {
    NSString * var1;
    NSString * var2;
}

这个对象的底层的数据结构,相当于是:

struct SomeObject {
    struct NSObject {
        struct objc_class {
            Class isa;
        };
    };
    NSString * var1;
    NSString * var2;
};

对象在内存中的情况:

Class isa;
NSString * var1;
NSString * var2;

所以说,OC中的对象是一个指向ClassObject地址的变量:
id obj = &ClassObject 。
对象的实例变量则是,ClassObject地址加上变量对应的偏移量:
void *ivar = &obj + offset(N)

OC中构成一个对象有三个部分:
创建好一个对象后,有一块首地址指向Class的内存,就是Class类型的 isa 指针,指向结构体 objc_class,其中类的成员变量,对象方法就存放在这。 objc_class 有superclass ,指向元类(metaclass),类方法就保存在其中。如下图:


让每一个类的 isa 指向对应的元类,这样就达到了使类方法和实例方法的调用机制相同的目的:
实例方法调用时,通过对象的 isa 在类中获取方法的实现
类方法调用时,通过类的 isa 在元类中获取方法的实现


在类继承体系中查询类型信息

可用类型信息查询方法来检视类继承体系:

  • isMemberOfClass: 能够判断出对象是否为某个特定类的实例
    [[NSMutableArray array] isMemberOfClass:[NSArray class]] = NO

  • isKindOfClass: 则能判断出是否为某类或派生类的实例
    [[NSMutableArray array] isKindOfClass:[NSArray class]] = YES

使用isa指针获取对象所属的类,然后通过super_class指针在继承体系中游走。由于对象是动态的,此特性显得极为重要。必须查询类型信息,方能完全了解对象的真实类型。

  1. 每个实例都有一个指向Class对象的指针,用以表明其类型,而这些Calss对象则构成类的继承体系
  2. 如果对象类型无法在编译期确定,那么就应该使用类型信息查询方法来探知。
  3. 尽量使用类型信息查询方法来确定对象类型,而不要直接比较类对象 [obj1 class] == [NSString class], 因为某些对象可能实现了消息转发功能。

OC对象创建

下面简单来看一下,+ alloc 和 - init,这两个方法,都做了什么。

+ (id)alloc {
    return _objc_rootAlloc(self);
}
// Base class implementation of +alloc. cls is not nil.
// Calls [cls allocWithZone:nil].
id
_objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
// Call [cls alloc] or [cls allocWithZone:nil], with appropriate 
// shortcutting optimizations.
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
    if (slowpath(checkNil && !cls)) return nil;

#if __OBJC2__
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        // No alloc/allocWithZone implementation. Go straight to the allocator.
        // fixme store hasCustomAWZ in the non-meta class and 
        // add it to canAllocFast's summary
        if (fastpath(cls->canAllocFast())) {
            // No ctors, raw isa, etc. Go straight to the metal.
            bool dtor = cls->hasCxxDtor();
            id obj = (id)calloc(1, cls->bits.fastInstanceSize());
            if (slowpath(!obj)) return callBadAllocHandler(cls);
            obj->initInstanceIsa(cls, dtor);
            return obj;
        }
        else {
            // Has ctor or raw isa or something. Use the slower path.
            id obj = class_createInstance(cls, 0);
            if (slowpath(!obj)) return callBadAllocHandler(cls);
            return obj;
        }
    }
#endif

    // No shortcuts available.
    if (allocWithZone) return [cls allocWithZone:nil];
    return [cls alloc];
}

1、allco方法,最主要的几行代码就是:
id obj = (id)calloc(1, cls->bits.fastInstanceSize());
obj.isa = (uintptr_t)cls;
在使用 calloc 为对象分配一块内存空间之前,我们要先获取对象在内存的大小,在获取对象大小之后,调用 calloc 函数就可以为对象分配内存空间了,接着就是写入isa指针,初始化引用计数器,以及重置所有实例变量。

有关初始化isa的内容,可查看:从 NSObject 的初始化了解 isa

2、init 方法:

- (id)init {
    return _objc_rootInit(self);
}
id
_objc_rootInit(id obj)
{
    // In practice, it will be hard to rely on this function.
    // Many classes do not properly chain -init calls.
    return obj;
}

从此可知 init 方法只是调用了 _objc_rootInit 并返回了当前对象
alloc 方法会返回一个合法的没有初始化的实例对象。每一个发送到实例的消息会被翻译为objc_msgSend() 函数的调用,它的参数是指向 alloc 返回的对象的、名为 self 的指针的。这样之后 self 已经可以执行所有方法了。
为了完成两步创建,第一个发送给新创建的实例的方法是约定俗成的 init 方法。


References

http://www.jianshu.com/p/fedbc5c2f189
http://www.jianshu.com/p/f725d2828a2f
http://www.sealiesoftware.com/blog/archive/2009/04/14/objc_explain_Classes_and_metaclasses.html

iOS日常
Web note ad 1