iOS开发进阶(唐巧)读书笔记(二)

第三部分:iOS开发底层原理

1、Objective-C对象模型

1.1 isa指针

NSObject.h部分代码:

NS_ROOT_CLASS
@interface NSObject <NSObject> {
    Class isa;
}

objc.h部分代码:

typedef struct objc_class *Class;
typedet struct objc_object {
    Class isa;
} *id;

每个对象都有一个名为isa的指针,指向该对象的类

isa指针指向流程图如下:

001isa指针指向流程图.png

如果把类看成一个C语言的结构体(struct),isa指针就是这个结构体的第一个成员变量,类的其他成员变量依次排列在结构体中

排列顺序:

1 isa指针
2 NSObject的成员变量
3 NSObject子类的成员变量
4 NSObject子类的子类的成员变量
... ...
n-1 父类的成员变量
n 类本身的成员变量

一个简单的继承的实例代码:

@interface Father : NSObject {
    int _father;
}

@end

@implementation Father
@end

@interface Child : Father {
    int _child;
}

@end

@implementation Child
@end

在Xcode中,我们看到如下截图,这个结构与上面说的一致

002Xcode截图.png

因为对象在内存中的排布可以看成一个结构体,该结构体的大小并不能动态变化,所以无法在运行时动态地给对象增加成员变量。

对象的方法定义都保存在类的可变区域中。
在下面的 Objective-C 1.0 中,我们可以看到方法的定义列表是一个名为 methodLists 的指针
通过修改指针指向的指针的值,就可以动态的为某一个类增加成员方法,这也是 Category 实现的原理

Objective-C 1.0 objc_class代码

struct objc_class {
    Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
    Class super _class
    const char *name
    long version
    long info
    long instance_size
    struct objc_ivar_list *ivars
    struvt objc_method_list **methodLists
    struct objc_cache *cache
    struct objc_protocol_list *protocols
#endIf
} OBJC2_UNAVAILABLE

1.2 动态创建对象

#import <objc/runtime.h>

...

- (void)dynamicCreateClass {
    // 创建一个名为CustomView的类,它是UIView的子类
    Class newClass = objc_allocateClassPair([UIView class], "CustomView", 0);
    // 为这个类增加一个report的方法
    class_addMethod(newClass, @selector(report), (IMP)ReportFunction, "v@:");
    // 注册该类
    objc_registerClassPair(newClass);
    
    // 创建一个newClass的实例对象
    id instanceOfNewClass = [[newClass alloc] init];
    // 调用report方法
    [instanceOfNewClass performSelector:@selector(report)];
}

void ReportFunction(id self, SEL _cmd) {
    NSLog(@"This object is %p", self);
    NSLog(@"Class is %@, and super is %@", [self class], [self superclass]);
    Class currentClass = [self class];
    for (int i = 1; i < 5; i++) {
        NSLog(@"Following the isa pointer %d times gives %@ = %p", i, currentClass, currentClass);
        // 获取对象的isa指针所指向的对象
        currentClass = object_getClass(currentClass);
    }
    NSLog(@"NSObject class is %@ = %p", [NSObject class], [NSObject class]);
    NSLog(@"NSObject meta class is %@ = %p", object_getClass([NSObject class]), object_getClass([NSObject class]));
}

代码关键点:

  1. import runtime 相关的头文件:objc/runtime.h
  2. 使用 objc_allocateClassPair 方法创建新的类。
  3. 使用 class_addMethod 方法来给类增加新的方法。
  4. 使用 objc_registerClassPair 方法来注册新的类。
  5. 使用 objc_getClass 方法来获取对象的isa指针指向的对象。

1.3 方法交换(Method Swizzling)API说明

Objective-C提供了以下API来动态替换类方法或实例方法的实现:

  • class_replaceMethod 替换类方法的定义
class_replaceMethod(Class  _Nullable __unsafe_unretained cls, SEL  _Nonnull name, IMP  _Nonnull imp, const char * _Nullable types)
  • method_exchangeImplementations 交换两个方法的实现
method_exchangeImplementations(Method  _Nonnull m1, Method  _Nonnull m2)
  • method_setImplementation 设置一个方法的实现
method_setImplementation(Method  _Nonnull m, IMP  _Nonnull imp)

比较:

  • class_replaceMethod 当类中没有找到要替换的原方法时,该方法会调用 class_addMethod 来为类增加一个新的方法,也正因为这样,class_replaceMethod 在调用时需要传入 type 参数,而 method_exchangeImplementationsmethod_setImplementation 都不需要
  • method_exchangeImplementations 内部实现是获取到两个方法的实现,然后进行互换

文档如下图:

003method_exchangeImplementations文档截图.png

使用场景:

  • class_replaceMethod 当需要替换的方法有可能不存在时,可以考虑使用该方法。
  • method_exchangeImplementations 当需要交换两个方法的实现时使用。
  • method_setImplementation 是最简单的用法,当仅仅需要为一个方法设置其实现方式时使用。

2、Tagged Pointer 对象

2.1 原有系统的问题

32位程序过渡到64位存在的问题:

  • 问题一:内存翻倍。
    在iOS数据类型中,很多数据类型所占内存都是根据CPU的位数决定的。那么,当程序从32位程序过渡到64位时,这些数据类型的内存就会翻倍。如下图所示:


    004内存翻倍示意图.png
  • 问题二:效率问题。
    为了存储和访问一个NSNumber对象,我们需要在堆上为其分配内存,另外还要维护它的引用计数,管理它的生命周期。这些都给程序增加了额外的逻辑,造成运行效率上的损失,

2.2 Tagged Pointer 介绍

Tagged Pointer就是为了解决上述问题提出的。
原理:将一个对象指针拆分为两部分。如下图:

005TaggedPointer结构示意图.png

引入后,内存变化如下图:

006优化后内存示意图.png

特点:

  1. 专门用来存储小的对象,例如 NSNumberNSDate
  2. 指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象’皮‘的普通变量而已。所以,它的内存并不存储在堆中,也不需要 mallocfree
  3. 在内存读取上有着以前3倍的效率,创建时比之前快106倍

注:Tagged Pointer 并不是真正的对象,而是一个伪对象,没有 isa 指针

2.2 64位下 isa 指针优化

32位环境:

对象的引用计数都保存在一个外部表中。

Retain 操作包含如下的5个步骤:

  1. 获取全局的记录引用计数的 hash 表。
  2. 为了线程安全,给该 hash 表加锁。
  3. 查找到目标对象的引用计数值。
  4. 将该引用计数值加1,写回 hash 表。
  5. 给该 hash 表解锁。

为了线程安全,需要对 hash 表进行加锁,从性能上看是非常差的。

64位环境:

isa指针是64位。每个bit位含义如下图:

bit位 变量名 意义
1 bit indexed 0 表示普通的isa,1 表示 Tagged Pointer
1 bit has_assoc 表示对象是否有过 associated 对象,如果没有,在析构释放内存时可以更快
1 bit has_cxx_dtor 表示该对象是否有 C++ 或 ARC 的析构函数,如果没有,在析构释放内存时可以更快
30 bit shiftcls 类的指针
9 bit magic 其值固定为 0xd2,用于在调试时分辨对象是否未完成初始化
1 bit weakly_referenced 表示该对象是否有过 weak 对象,如果没有,在析构释放内存时可以更快
1 bit deallocating 表示该对象是否正在析构
1 bit has_sidetable_rc 表示该对象的引用计数值是否大到无法直接在 isa 中保存
19 bit extra_rc 表示该对象超过 1 的引用计数值,例如,如果该对象的引用计数是6,则 extra_rc 的值为5

extra_rc 的19位 bit 用来保存对象的引用计数,这样对引用计数的操作只需要修改这个职责即可。

Retain 操作包含如下的5个步骤:

  1. 检查 isa 指针上面的标记位,看引用计数是否保存在 isa 变量中,如果不是,则使用以前的步骤,否则执行第2步。
  2. 检查当前对象是否正在释放,如果是,则不做任何事情。
  3. 增加该对象的引用计数,但是并不马上写回到 isa 变量中。
  4. 检查增加后的引用计数的值是否能够被19位表示,如果不是,则切换为以前的办法,否则执行第5步。
  5. 进行一个原子的写操作,将 isa 的值写回。

3、block 对象模型

3.1 定义:

在苹果的 llvm 项目的开源代码(https://llvm.org/svn/llvm-project/compiler-rt/tags/Apple/Libcompiler_rt-10/BlocksRuntime/Block_private.h)中,我们可以看到 block 的数据结构定义,如下图:

007block的数据结构.png

对应的结构体定义如下:

struct Block_descriptor {
    unsigned long int reserved;
    unsigned long int size;
    void (*copy)(void *dst, void *src);
    void (*dispose)(void *);
};

struct Block_layout {
    void *isa;
    int flags;
    int reserved;
    void (*invoke)(void *, ...);
    struct Block_descriptor *descriptor;
    /* Imported variables */
};

组成 block 实例的6个部分:

  1. isa 指针,所有对象都有该指针,用于实现对象的相关的功能。
  2. flags 用于按 bit 位表示一些 block 的附加信息,在后面介绍的 block copy 的实现代码中可以看到该变量的使用。
  3. reserved 保留变量。
  4. invoke 函数指针,指向具体的 block 实现的函数调用地址。
  5. descriptor 表示该 block 的附加描述信息,主要是 size 的大小,以及 copy 和 dispose 函数的指针。
  6. variable capture 过来的变量,block 能够访问它外部的局部变量,就是因为将这些变量(或变量的地址)复制到了结构体中。

3.2 分类:

block 的类型:

  1. _NSConcreteGlobalBlock 全局的静态 block,不会访问任何外部变量。
  2. _NSConcreteStackBlock 保存在栈中的 block,当函数返回时会被销毁。
  3. _NSConcreteMallocBlock 保存在堆中的 block,当引用计数为 0 时会被销毁。

注:用 clang 分析 block 实现

clang 提供了一个命令,可以将 Objective-C 的源码改写成C语言。
命令是:clang -rewrite-objc block.c

3.2.1 NSConcreteGlobalBlock 类型的 block 的实现

创建一个名字为 block1.c 的源文件,文件实现:

#include <stdio.h>

int main(int argc, char const *argv[]) {
    ^{ printf("Hello, World!\n"); } ();
    return 0;
}

在命令行中输入 clang -rewrite-objc block1.c,即可在目录中看到 clang 输出了一个名为 ”block1.cpp” 的文件,这个文件就是 block 在C语言中的实现。
关键代码引用如下:

...
struct __block_impl {
  void *isa;
  int Flags;
  int Reserved;
  void *FuncPtr;
};
...
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
 printf("Hello, World!\n"); }

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, char const *argv[])
{
    ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA)) ();
    return 0;
}

代码中,__main_block_impl_0 就是该 block 的实现,从中我们可以看出:

  1. 一个 block 实际是一个对象,它主要由一个 isa、一个 impl 和一个 descriptor 组成。
  2. 由于这里没有开启 ARC,所以我们看到 isa 的指向还是 _NSConcreteStackBlock。但在开启 ARC 时,block 应该是 _NSConcreteGlobalBlock 类。
  3. impl 是实际函数指针,本例中,它指向 __main_block_func_0。这里的 impl 相当于之前提到的 invoke 变量,只是 clang 编译器对变量的命名不一样而已。
  4. descriptor 是用于描述当前这个 block 的附加信息的,包括结构体的大小,需要 capturedispose 的变量列表等。
    结构体大小需要保存到原因是,每个 blockcapture 一些变量,这里变量会加到 __main_block_impl_0 这个结构体中,使其体积变大。

具体文件见:https://github.com/AlonerOwl/OC_Block/tree/master/NSConcreteGlobalBlock

3.2.2 NSConcreteStackBlock 类型的 block 的实现

创建一个名字为 block1.c 的源文件,文件实现:

#include <stdio.h>

int main(int argc, char const *argv[]) {
    int a = 100;
    void (^block2)(void) = ^{ // block 实现
        printf("%d\n", a);
    };
    block2();
    
    return 0;
}

clang 后:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  int a;
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _a, int flags=0) : a(_a) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  int a = __cself->a; // bound by copy

        printf("%d\n", a);
    }

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};
int main(int argc, char const *argv[]) {
    int a = 100;
    void (*block2)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, a));
    ((void (*)(__block_impl *))((__block_impl *)block2)->FuncPtr)((__block_impl *)block2);

    return 0;
}

在本例中,我们可以看到:

  1. 本例中,isa 指向 _NSConcreteStackBlock,说明这是一个分配在栈上的实例。
  2. __main_block_impl_0 中增加一个变量 a,在 block 中引用的变量 a,实际是在声明 block 时,被复制到 __main_block_impl_0 结构体中的那个变量 a
  3. __main_block_impl_0 中由于增加一个变量 a,所以结构体变大了,该结构体大小被写在了 __main_block_desc_0 中

我们修改上面的源码,在变量前面增加 __block 关键字:

#include <stdio.h>

int main(int argc, char const *argv[]) {
    __block int i = 1024;
    void (^block2)(void) = ^{ // block 实现
        printf("%d\n", i);
        i = 1023;
    };
    block2();
    
    return 0;
}

clang 后,与之前差异相当大:

struct __Block_byref_i_0 {
  void *__isa;
__Block_byref_i_0 *__forwarding;
 int __flags;
 int __size;
 int I;
};

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
  __Block_byref_i_0 *i; // by ref
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_i_0 *_i, int flags=0) : i(_i->__forwarding) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
  __Block_byref_i_0 *i = __cself->i; // bound by ref

        printf("%d\n", (i->__forwarding->i));
        (i->__forwarding->i) = 1023;
    }
static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {_Block_object_assign((void*)&dst->i, (void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {_Block_object_dispose((void*)src->i, 8/*BLOCK_FIELD_IS_BYREF*/);}

static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0), __main_block_copy_0, __main_block_dispose_0};

int main(int argc, char const *argv[]) {
    __attribute__((__blocks__(byref))) __Block_byref_i_0 i = {(void*)0,(__Block_byref_i_0 *)&i, 0, sizeof(__Block_byref_i_0), 1024};
    void (*block2)(void) = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_i_0 *)&i, 570425344));
    ((void (*)(__block_impl *))((__block_impl *)block2)->FuncPtr)((__block_impl *)block2);

    return 0;
}

从代码中我们可以看到:

  1. 源码中增加了一个名为 __Block_byref_i_0 的结构体,用于保存我们要 capture 并且修改的变量 i
  2. __main_block_impl_0 中引用的是 __Block_byref_i_0 的结构体指针,这样就可以起到修改外部变量的作用。
  3. __Block_byref_i_0 的结构体带有 isa,说明它也是一个对象。
  4. 我们需要负责 __Block_byref_i_0 结构体相关的内存管理,所有 __main_block_desc_0 中增加了 copydispose 函数指针,用于在调用前后修改相应变量的引用计数。

具体文件见:https://github.com/AlonerOwl/OC_Block/tree/master/NSConcreteStackBlock

总结:
block 对于外部变量的使用,非 __block 修饰的变量,直接将其复制到 block 数据结构中来实现访问;__block 修饰的变量,复制这个变量的引用地址来实现访问的。

3.2.3 NSConcreteMallocBlock 类型的 block 的实现

NSConcreteMallocBlock 类型的 block 通常不会在源码中直接出现,只有当一个 block 被调用其 copy 方法的时候,系统才会将这个 block 复制到堆中,从而产生 NSConcreteMallocBlock 类型的 block。

注:在 ARC 开启的情况下,将只会存在 NSConcreteGlobalBlockNSConcreteMallocBlock 类型的 block。原来的 NSConcreteStackBlock 会被 NSConcreteMallocBlock 的进行替代。
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,047评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,807评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,501评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,839评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,951评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,117评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,188评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,929评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,372评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,679评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,837评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,536评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,168评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,886评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,129评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,665评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,739评论 2 351

推荐阅读更多精彩内容