4

谈谈ivar的直接访问

 3 years ago
source link: http://satanwoo.github.io/2018/02/04/iOS-iVar/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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

最近对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,看来也会对包大小产生不可忽视的影响。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK