谈谈ivar的直接访问

大水文一篇
大水文一篇
大水文一篇

起因

最近对Block的一些实现细节又进行了一次复习,主要涉及的是捕捉变量的部分。有一个点我之前一直没太关注:对ivar变量的直接访问为啥会产生循环引用。

在我原先的理解中,之所以会产生循环引用,绝大多数场景都是由于block里面涉及了self关键字,比如[self doSomething](同理,对于property的访问本质也是一堆方法),但是为啥对ivar的访问也会导致循环引用呢?

不是直接采用 *(void *)address = xxx这样的直接对编译好的静态地址赋值就好了?

当时傻逼了,写完本文后想想就算编译成地址了,基地址从哪算还是要依赖self变量。

谈谈ivar的访问是啥形式

还是回到runtime来看看吧,万变不离其宗,从objc_class结构体看起:

struct objc_class : objc_object {
    // Class ISA; // 8byte
    Class superclass; // 8byte
    cache_t cache;             // formerly cache pointer and vtable // 4 + 4 + 8
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags

    class_rw_t *data() { 
        return bits.data();
    }

主要的运行时数据都是class_rw_t表示,继续瞅瞅:

struct class_rw_t {
    // Be warned that Symbolication knows the layout of this structure.
    uint32_t flags;
    uint32_t version;

    const class_ro_t *ro;

其中class_ro_t基本上是从二进制产物中读取的“副本”数据,我们看看:

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;
#ifdef __LP64__
    uint32_t reserved;
#endif

    const uint8_t * ivarLayout;

    const char * name;
    method_list_t * baseMethodList;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;

看起来ivar_list_t就是存放ivar的列表,他的实现是一个模版类,看看具体结构表示:

struct entsize_list_tt {
    uint32_t entsizeAndFlags;
    uint32_t count;
    Element first;

具体对应ivar,替换掉模版就是:

struct ivar_list_t {
    uint32_t entsizeAndFlags;
    uint32_t count;
    ivar_t first;

其中,ivar_t表征的就是我们每个ivar

int32_t *offset;
const char *name;
const char *type;

嗯,从这里开始offset是用一个int32_t *的指针来表示,就开始有意思了。这里我们先暂时忽略

看起来,如果按照这种方式访问ivar,整个流程要经过好多次指针转移:

class -> class.rw_data -> class.rw_data.ro_data -> class.rw_data.ro_data.ivars -> 
-> class.rw_data.ro_data.ivars.first[n]

如果是这样,大量使用ivar肯定很耗时。那么,对于ivar的访问究竟是怎么玩的呢?

全局变量

我们用如下这个非常简单的例子来瞅瞅:

typedef void(^MyBlock)(void);

@interface MyObject : NSObject
@property (nonatomic) NSUInteger haha;
@property (nonatomic, copy) MyBlock block;

- (void)inits;

@end

@implementation MyObject
- (void)inits
{
    self.block = ^{
        _haha = 5;
    };
}
@end

int main(int argc, char * argv[]) {
    MyObject *object = [MyObject new];
    [object inits];
}

重写一把,基本转化成如下的形式:

typedef void(*MyBlock)(void);


#ifndef _REWRITER_typedef_MyObject
#define _REWRITER_typedef_MyObject
typedef struct objc_object MyObject;
typedef struct {} _objc_exc_MyObject;
#endif

// 注意点1!!!!!!!!!!!!!!!!!!!!
extern "C" unsigned long OBJC_IVAR_$_MyObject$_haha;
extern "C" unsigned long OBJC_IVAR_$_MyObject$_block;
struct MyObject_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    NSUInteger _haha;
    MyBlock _block;
};

// @property (nonatomic) NSUInteger haha;
// @property (nonatomic, copy) MyBlock block;

// - (void)inits;

/* @end */


// @implementation MyObject

struct __MyObject__inits_block_impl_0 {
  struct __block_impl impl;
  struct __MyObject__inits_block_desc_0* Desc;
  MyObject *self;

  // 注意点2!!!!!!!!!!!!!!!
  __MyObject__inits_block_impl_0(void *fp, struct __MyObject__inits_block_desc_0 *desc, MyObject *_self, int flags=0) : self(_self) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

// 注意点3!!!!!!!!!!!!
static void __MyObject__inits_block_func_0(struct __MyObject__inits_block_impl_0 *__cself) {
  MyObject *self = __cself->self; // bound by copy

        (*(NSUInteger *)((char *)self + OBJC_IVAR_$_MyObject$_haha)) = 5;
    }
static void __MyObject__inits_block_copy_0(struct __MyObject__inits_block_impl_0*dst, struct __MyObject__inits_block_impl_0*src) {_Block_object_assign((void*)&dst->self, (void*)src->self, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static void __MyObject__inits_block_dispose_0(struct __MyObject__inits_block_impl_0*src) {_Block_object_dispose((void*)src->self, 3/*BLOCK_FIELD_IS_OBJECT*/);}

static struct __MyObject__inits_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __MyObject__inits_block_impl_0*, struct __MyObject__inits_block_impl_0*);
  void (*dispose)(struct __MyObject__inits_block_impl_0*);
} __MyObject__inits_block_desc_0_DATA = { 0, sizeof(struct __MyObject__inits_block_impl_0), __MyObject__inits_block_copy_0, __MyObject__inits_block_dispose_0};

static void _I_MyObject_inits(MyObject * self, SEL _cmd) {
    ((void (*)(id, SEL, MyBlock))(void *)objc_msgSend)((id)self, sel_registerName("setBlock:"), ((void (*)())&__MyObject__inits_block_impl_0((void *)__MyObject__inits_block_func_0, &__MyObject__inits_block_desc_0_DATA, self, 570425344)));
}

static NSUInteger _I_MyObject_haha(MyObject * self, SEL _cmd) { return (*(NSUInteger *)((char *)self + OBJC_IVAR_$_MyObject$_haha)); }
static void _I_MyObject_setHaha_(MyObject * self, SEL _cmd, NSUInteger haha) { (*(NSUInteger *)((char *)self + OBJC_IVAR_$_MyObject$_haha)) = haha; }

static void(* _I_MyObject_block(MyObject * self, SEL _cmd) )(){ return (*(MyBlock *)((char *)self + OBJC_IVAR_$_MyObject$_block)); }
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);

static void _I_MyObject_setBlock_(MyObject * self, SEL _cmd, MyBlock block) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct MyObject, _block), (id)block, 0, 1); }
// @end

int main(int argc, char * argv[]) {
    MyObject *object = ((MyObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("MyObject"), sel_registerName("new"));
    ((void (*)(id, SEL))(void *)objc_msgSend)((id)object, sel_registerName("inits"));
}

一大堆东西,没啥特别的地方,我们只要关注几个地方:

  • 对于每个ivar,都有对应的全局变量

    extern "C" unsigned long OBJC_IVAR_$_MyObject$_haha;
    extern "C" unsigned long OBJC_IVAR_$_MyObject$_block;
    
  • block_invoke对应的实现是通过对象自身作为基地址,全局变量作为偏移去对haha这个ivar进行赋值。

    static void __MyObject__inits_block_func_0(struct __MyObject__inits_block_impl_0 *__cself) {
      MyObject *self = __cself->self; // bound by copy
    
            (*(NSUInteger *)((char *)self + OBJC_IVAR_$_MyObject$_haha)) = 5;
        }
    
  • block的构造函数,确实捕捉了self

    __MyObject__inits_block_impl_0(void *fp, struct __MyObject__inits_block_desc_0 *desc, MyObject *_self, int flags=0) : self(_self) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
      }
    

由于全局变量的地址是在编译期就确定了,所以这里也就不难解释ivar_t里面为什么要保存int32_t *,保存的就是对应的全局变量地址。而全局变量的值则是对应的动态偏移。

结语

水完了,其实虽然runtime的结构体设计的比较绕,但是最后对于变量的访问和很多静态语言设计一样,也不会损失很多性能。

从另外一个角度看,如果声明了巨多的ivar,看来也会对包大小产生不可忽视的影响。