老青菜

iOS Modern Runtime

2017-08-11

上一篇文章 我们分析了 objc 1.0 的内存布局以及可能会出现的问题。接下来我们来看 Modern Runtime(objc 2.0)的内存布局,打开 objc4-723 ,或者 下载源码

objc_object

objc_object objc 中的对象,这个结构体在两个版本中没有什么变化,打开 objc-private.h,找到结构体定义。

struct objc_object {
private:
    isa_t isa;
public:
    // ISA() assumes this is NOT a tagged pointer object
    Class ISA();
    // getIsa() allows this to be a tagged pointer object
    Class getIsa();
    //此处省略 ...
}
//isa_t 是一个联合结构体
union isa_t 
{
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }
    Class cls;
    uintptr_t bits;
}

isa : 指向 MetaClass(元类)的指针。

objc_class 2.0

我们再来看一下 modern runtime 里做了哪些修改,打开 objc-runtime-new.h,找到结构体定义,

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

    class_rw_t *data() { 
        return bits.data();
    }
    //后面省略...
}

我们发现结构体里没有 ivarsmethodListsprotocols,多了一个 bits,看注释 class_rw_t * plus custom rr/alloc flags,大致意思是 bits 等于 class_rw_t * 指针加上自定义的 rr/alloc 标志,到底有什么用呢?

bits

我们先看看 class_data_bits_t 结构体。

struct class_data_bits_t {

    // Values are the FAST_ flags above.
    uintptr_t bits;
private:
    bool getBit(uintptr_t bit)
    {
        return bits & bit;
    }
//此处省略部分代码...
public:

    class_rw_t* data() {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
    void setData(class_rw_t *newData)
    {
        assert(!data()  ||  (newData->flags & (RW_REALIZING | RW_FUTURE)));
        // Set during realization or construction only. No locking needed.
        // Use a store-release fence because there may be concurrent
        // readers of data and data's contents.
        uintptr_t newBits = (bits & ~FAST_DATA_MASK) | (uintptr_t)newData;
        atomic_thread_fence(memory_order_release);
        bits = newBits;
    }
    //此处省略部分代码...
}

我们发现 class_data_bits_t.data() 等价于 objc_class.data(),方法内部将 bitsFAST_DATA_MASK 进行位运算,返回了 class_rw_t 结构体。我们找到 FAST_DATA_MASK 定义:

#if !__LP64__

//不是64位

// class is a Swift class
#define FAST_IS_SWIFT         (1UL<<0)
// class or superclass has default retain/release/autorelease/retainCount/
//   _tryRetain/_isDeallocating/retainWeakReference/allowsWeakReference
#define FAST_HAS_DEFAULT_RR   (1UL<<1)
// data pointer
#define FAST_DATA_MASK        0xfffffffcUL

#elif 1

//因为elif 1,所以会进入这个判断

// class is a Swift class
#define FAST_IS_SWIFT           (1UL<<0)
// class or superclass has default retain/release/autorelease/retainCount/
//   _tryRetain/_isDeallocating/retainWeakReference/allowsWeakReference
#define FAST_HAS_DEFAULT_RR     (1UL<<1)
// class's instances requires raw isa
#define FAST_REQUIRES_RAW_ISA   (1UL<<2)
// data pointer
#define FAST_DATA_MASK          0x00007ffffffffff8UL

#else
//省略部分代码...
#endif

class_rw_t 是什么呢?我们接着往下看。

class_rw_t

存储属性、协议、方法等等类信息,是可读写的,包含了编译期间确定类的信息和运行期间(Category)确定的类信息

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;
    method_array_t methods;
    property_array_t properties;
    protocol_array_t protocols;

    Class firstSubclass;
    Class nextSiblingClass;
    char *demangledName;
#if SUPPORT_INDEXED_ISA
    uint32_t index;
#endif
    //后面省略...
};

仔细看还有一个 class_ro_t 常量指针 ro ,表示值不能修改,顺便回顾一下常量指针的概念。

常量指针:指针指向常量变量,指针指向的值不能修改。
const char p = ‘a’, char q = ‘b’;
*p = ‘b’;//error
p[1] = ‘b’;//error
p = q;

指针常量:const与指针变量紧邻,指针变量不允许修改,定义指针的时候,必须要给一个确定地址。
char const p = ‘a’, char q = ‘b’;
p = ‘b’; //error
p = q; //error
*p = ‘A’;//ok

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;

    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;
    method_list_t *baseMethods() const {
        return baseMethodList;
    }
};

编译期

接下来我们看一下编译期间,objc_class 的结构是怎么样的。 在编译期间,class_data_bits_t *data 指向的是一个 class_ro_t * 指针。为什么这么说呢?我们做一个测试,新建一个 KDClangGrandsonTest Class,代码如下:

@interface KDClangGrandsonTest()
/** 昵称 */
@property (nonatomic, strong) NSString *nickname;
/** 年龄 */
@property (nonatomic, assign) NSInteger   age;
@end

@implementation KDClangGrandsonTest
{
    NSString    *_sex;
}
@end

在终端里执行:

clang -rewrite-objc KDClangGrandsonTest.m

打开 KDClangGrandsonTest.cpp,找到以下代码:

//类 结构体
struct _class_t {
    struct _class_t *isa;
    struct _class_t *superclass;
    void *cache;
    void *vtable;
    struct _class_ro_t *ro;
};

我们看到 class_t 包含了 ro 成员,搜索 _class_ro_t,找到以下代码:

struct _class_ro_t {
    unsigned int flags;
    unsigned int instanceStart;
    unsigned int instanceSize;
    unsigned int reserved;
    const unsigned char *ivarLayout;
    const char *name;
    const struct _method_list_t *baseMethods;
    const struct _objc_protocol_list *baseProtocols;
    const struct _ivar_list_t *ivars;
    const unsigned char *weakIvarLayout;
    const struct _prop_list_t *properties;
};

//类构造函数
extern "C" __declspec(dllexport) struct _class_t OBJC_CLASS_$_KDClangGrandsonTest __attribute__ ((used, section ("__DATA,__objc_data"))) = {
    0, // &OBJC_METACLASS_$_KDClangGrandsonTest,
    0, // &OBJC_CLASS_$_KDClangSonTest,
    0, // (void *)&_objc_empty_cache,
    0, // unused, was (void *)&_objc_empty_vtable,
    &_OBJC_CLASS_RO_$_KDClangGrandsonTest,
};

//编译期间 类信息结构体
static struct _class_ro_t _OBJC_CLASS_RO_$_KDClangGrandsonTest __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    0, __OFFSETOFIVAR__(struct KDClangGrandsonTest, _sex), sizeof(struct KDClangGrandsonTest_IMPL), 
    (unsigned int)0, 
    0, 
    "KDClangGrandsonTest",
    (const struct _method_list_t *)&_OBJC_$_INSTANCE_METHODS_KDClangGrandsonTest,
    0, 
    (const struct _ivar_list_t *)&_OBJC_$_INSTANCE_VARIABLES_KDClangGrandsonTest,
    0, 
    (const struct _prop_list_t *)&_OBJC_$_PROP_LIST_KDClangGrandsonTest,
};

//成员变量 结构体
static struct /*_ivar_list_t*/ {
    unsigned int entsize;  // sizeof(struct _prop_t)
    unsigned int count;
    struct _ivar_t ivar_list[3];
} _OBJC_$_INSTANCE_VARIABLES_KDClangGrandsonTest __attribute__ ((used, section ("__DATA,__objc_const"))) = {
    sizeof(_ivar_t),
    3,
    {{(unsigned long int *)&OBJC_IVAR_$_KDClangGrandsonTest$_sex, "_sex", "@\"NSString\"", 3, 8},
	 {(unsigned long int *)&OBJC_IVAR_$_KDClangGrandsonTest$_age, "_age", "q", 3, 8},
	 {(unsigned long int *)&OBJC_IVAR_$_KDClangGrandsonTest$_nickname, "_nickname", "@\"NSString\"", 3, 8}}
};

不难发现:

  1. 在编译的时候就确定了 ivars,包含了 _sex_age_nickname
  2. _OBJC_CLASS_RO_$_KDClangGrandsonTest 函数内部初始化了类的信息,包含 _method_list_t_ivar_list_t_prop_list_t 等等信息,并返回 _class_ro_t 结构体。

我们发现编译后,没有生成 class_rw_t 结构体,也搜索不到相关信息,那么 class_rw_t 又是怎么产生的呢?

运行时 Non Fragile

objc 2.0 中引入了 Non Fragile,它有什么作用呢?我们来看 官方描述:

The most notable new feature is that instance variables in the modern runtime are “non-fragile”:
In the modern runtime, if you change the layout of instance variables in a class, you do not have to recompile classes that inherit from it.

在最新的 runtime 里,如果修改了一个类的布局,你不需要重新编译继承这个类的所有子类,系统会帮你做好这件事。
App 启动 这一篇文章里,我们了解到在 dylb 通知 objc prepare images 的时候,runtime 会去更新 ivars offset

Non-fragile ivars offsets updated

具体是怎么做到呢?我们来看下 runtime 源码。首先 objc runtime 收到 prepare images 的通知,runtime 开始初始化,调用 _read_images ,具体可以看 objc-runtime-new.mm

_read_images

void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
    header_info *hi;
    uint32_t hIndex;
    size_t count;
    size_t i;
    Class *resolvedFutureClasses = nil;
     //加载类...

    //注册方法...

    //加载协议...

    //重新布局class...
    // Realize non-lazy classes (for +load methods and static instances)
    for (EACH_HEADER) {
        classref_t *classlist = 
            _getObjc2NonlazyClassList(hi, &count);
        for (i = 0; i < count; i++) {
            Class cls = remapClass(classlist[i]);
            if (!cls) continue;

            // hack for class __ARCLite__, which didn't get this above
#if TARGET_OS_SIMULATOR
            if (cls->cache._buckets == (void*)&_objc_empty_cache  &&  
                (cls->cache._mask  ||  cls->cache._occupied)) 
            {
                cls->cache._mask = 0;
                cls->cache._occupied = 0;
            }
            if (cls->ISA()->cache._buckets == (void*)&_objc_empty_cache  &&  
                (cls->ISA()->cache._mask  ||  cls->ISA()->cache._occupied)) 
            {
                cls->ISA()->cache._mask = 0;
                cls->ISA()->cache._occupied = 0;
            }
#endif
            realizeClass(cls);
        }
    }

    //加载 category...
}

_read_images 先注册了 class 信息(方法、协议),然后调用 realizeClass 重新布局 class。我们来看下是怎么布局的:

realizeClass

static Class realizeClass(Class cls)
{
    runtimeLock.assertWriting();
    const class_ro_t *ro;
    class_rw_t *rw;
    Class supercls;
    Class metacls;
    bool isMeta;

    if (!cls) return nil;
    if (cls->isRealized()) return cls;
    assert(cls == remapClass(cls));

    // fixme verify class is not in an un-dlopened part of the shared cache?
    //获取data,编译期间 data 指向 class_ro_t
    ro = (const class_ro_t *)cls->data();
    if (ro->flags & RO_FUTURE) {
        //已经转换过 class_rw_t
        // This was a future class. rw data is already allocated.
        rw = cls->data();
        ro = cls->data()->ro;
        cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
    } else {
        //还没有转换 class_rw_t,创建 class_rw_t,更新 cls data
        // Normal class. Allocate writeable class data.
        rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
        rw->ro = ro;
        rw->flags = RW_REALIZED|RW_REALIZING;
        cls->setData(rw);
    }

    //更新 ivars offset
    // Reconcile instance variable offsets / layout.
    // This may reallocate class_ro_t, updating our ro variable.
    if (supercls  &&  !isMeta) reconcileInstanceVariables(cls, supercls, ro);

    // Set fastInstanceSize if it wasn't set already.
    cls->setInstanceSize(ro->instanceSize);

    //加载 method,并且加载 category
    // Attach categories
    methodizeClass(cls);

    return cls;
}

总结一下,流程如下:

  1. 获取 class data(编译期间 data 指向 class_ro_t)。
  2. 如果 class_rw_t 没有准备好,初始化一个 class_rw_t,设置 roflag
  3. 设置 class 的 data。
  4. 调用 reconcileInstanceVariables,更新 ivars offset,这一步就是 Non Fragile 的关键。
  5. 调用 methodizeClass 加载类信息 methodspropertyprotocol,然后加载 category

reconcileInstanceVariables

我们先来看 reconcileInstanceVariables 是如何更新 ivars offset 的。

static void reconcileInstanceVariables(Class cls, Class supercls, const class_ro_t*& ro) 
{
     class_rw_t *rw = cls->data();
    //子类的 instanceStart 大于父类的 instanceSize,不需要调整
    if (ro->instanceStart >= super_ro->instanceSize) {
        // Superclass has not overgrown its space. We're done here.
        return;
    }
    // 子类的 instanceStart 小于父类的 instanceSize,需要调整
    if (ro->instanceStart < super_ro->instanceSize) {
        // Superclass has changed size. This class's ivars must move.
        // Also slide layout bits in parallel.
        // This code is incapable of compacting the subclass to 
        //   compensate for a superclass that shrunk, so don't do that.
        class_ro_t *ro_w = make_ro_writeable(rw);
        ro = rw->ro;
        moveIvars(ro_w, super_ro->instanceSize);
        gdb_objc_class_changed(cls, OBJC_CLASS_IVARS_CHANGED, ro->name);
    }
}

很清楚的看到,这里对比了子类的 instanceStart 和父类的instanceSize`,这两个属性都是在编译阶段就确定下来的。具体判断如下:

  1. 如果子类的instanceStart >= 父类的instanceSize,说明父类没有新增成员变量,或者子类已经偏移过了,不需要继续偏移了。
  2. 如果子类的instanceStart < 父类的instanceSize,说明父类新增了成员变量,子类的成员变量需要进行偏移。

moveIvars

那么 moveIvars 是如何修正 offset 的呢?其实并不复杂,代码如下:

static void moveIvars(class_ro_t *ro, uint32_t superSize)
{
    runtimeLock.assertWriting();

    uint32_t diff;

    //偏移差值是父类的instanceSize减去子类的instanceStart
    diff = superSize - ro->instanceStart;

    if (ro->ivars) {
        // Find maximum alignment in this class's ivars
        uint32_t maxAlignment = 1;
        for (const auto& ivar : *ro->ivars) {
            if (!ivar.offset) continue;  // anonymous bitfield

            uint32_t alignment = ivar.alignment();
            if (alignment > maxAlignment) maxAlignment = alignment;
        }

         //这里应该是字节对齐
        // Compute a slide value that preserves that alignment
        uint32_t alignMask = maxAlignment - 1;
        diff = (diff + alignMask) & ~alignMask;

        // Slide all of this class's ivars en masse
        for (const auto& ivar : *ro->ivars) {
            if (!ivar.offset) continue;  // anonymous bitfield
              // 老的 偏移量
            uint32_t oldOffset = (uint32_t)*ivar.offset;
            // 新的偏移量 = old + offset
            uint32_t newOffset = oldOffset + diff;
            //更新 offset
            *ivar.offset = newOffset;
            }
        }
    }
    *(uint32_t *)&ro->instanceStart += diff;
    *(uint32_t *)&ro->instanceSize += diff;
}

moveIvars 先计算了 需要偏移的差值 diffdiff = 父类instanceSize - 子类instanceStart,然后遍历所有的 ivar,加上偏移差值。
到这里,Non Fragile 已经实现了,我们继续看一下类信息和 category 的加载。

methodizeClass

/***********************************************************************
* methodizeClass
* Fixes up cls's method list, protocol list, and property list.
* Attaches any outstanding categories.
* Locking: runtimeLock must be held by the caller
**********************************************************************/
static void methodizeClass(Class cls)
{
    runtimeLock.assertWriting();

    bool isMeta = cls->isMetaClass();
    auto rw = cls->data();
    auto ro = rw->ro;

     //加载 method_list_t
    // Install methods and properties that the class implements itself.
    method_list_t *list = ro->baseMethods();
    if (list) {
        prepareMethodLists(cls, &list, 1, YES, isBundleClass(cls));
        rw->methods.attachLists(&list, 1);
    }
     //加载 property_list_t
    property_list_t *proplist = ro->baseProperties;
    if (proplist) {
        rw->properties.attachLists(&proplist, 1);
    }
     //加载 protocol_list_t
    protocol_list_t *protolist = ro->baseProtocols;
    if (protolist) {
        rw->protocols.attachLists(&protolist, 1);
    }

     //加载 category
    // Attach categories.
    category_list *cats = unattachedCategoriesForClass(cls, true /*realizing*/);
    attachCategories(cls, cats, false /*don't flush caches*/);
}

这里修正了类信息 method listprotocol listproperty list,然后加载 category,这里就不详细介绍 category 加载了,可以看我写的
ios-category

总结

最后,我们来总结一下:

  1. 编译期间,class data 指向 class_ro_t,包含 _method_list_t_ivar_list_t_prop_list_t 等等信息。
  2. 运行时,收到 dylbobjc prepare images 通知,开始初始化 runtime,初始化 class_rw_t,并指向 class data
  3. 更新 ivars offset,完成二进制兼容。
  4. 加载类信息 method_list_tproperty_list_tmethod protocol_list_t
  5. 加载 category

参考链接

Apple OpenSource objc4-723
Apple OpenSource Download
Apple Developer Documentation
深入解析 ObjC 中方法的结构
Non Fragile ivars

Tags: objc
使用支付宝打赏
使用微信打赏

若你觉得我的文章对你有帮助,欢迎点击上方按钮对我打赏

扫描二维码,分享此文章