【原】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指针指向流程图如下:
如果把类看成一个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中,我们看到如下截图,这个结构与上面说的一致
因为对象在内存中的排布可以看成一个结构体,该结构体的大小并不能动态变化,所以无法在运行时动态地给对象增加成员变量。
对象的方法定义都保存在类的可变区域中。
在下面的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])); }
代码关键点:
-
import runtime
相关的头文件:objc/runtime.h
。 - 使用
objc_allocateclasspair
方法创建新的类。 - 使用
class_addmethod
方法来给类增加新的方法。 - 使用
objc_registerclasspair
方法来注册新的类。 - 使用
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_exchangeimplementations
和method_setimplementation
都不需要method_exchangeimplementations
内部实现是获取到两个方法的实现,然后进行互换文档如下图:
使用场景:
-
class_replacemethod
当需要替换的方法有可能不存在时,可以考虑使用该方法。 -
method_exchangeimplementations
当需要交换两个方法的实现时使用。 -
method_setimplementation
是最简单的用法,当仅仅需要为一个方法设置其实现方式时使用。
2、tagged pointer 对象
2.1 原有系统的问题
32位程序过渡到64位存在的问题:
- 问题一:内存翻倍。
在ios数据类型中,很多数据类型所占内存都是根据cpu的位数决定的。那么,当程序从32位程序过渡到64位时,这些数据类型的内存就会翻倍。如下图所示:
- 问题二:效率问题。
为了存储和访问一个nsnumber对象,我们需要在堆上为其分配内存,另外还要维护它的引用计数,管理它的生命周期。这些都给程序增加了额外的逻辑,造成运行效率上的损失,
2.2 tagged pointer 介绍
tagged pointer就是为了解决上述问题提出的。
原理:将一个对象指针拆分为两部分。如下图:
引入后,内存变化如下图:
特点:
- 专门用来存储小的对象,例如
nsnumber
和nsdate
- 指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象’皮‘的普通变量而已。所以,它的内存并不存储在堆中,也不需要
malloc
和free
- 在内存读取上有着以前3倍的效率,创建时比之前快106倍
注:tagged pointer
并不是真正的对象,而是一个伪对象,没有 isa
指针
2.2 64位下 isa 指针优化
32位环境:
对象的引用计数都保存在一个外部表中。
retain
操作包含如下的5个步骤:
- 获取全局的记录引用计数的
hash
表。 - 为了线程安全,给该
hash
表加锁。 - 查找到目标对象的引用计数值。
- 将该引用计数值加1,写回
hash
表。 - 给该
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个步骤:
- 检查 isa 指针上面的标记位,看引用计数是否保存在 isa 变量中,如果不是,则使用以前的步骤,否则执行第2步。
- 检查当前对象是否正在释放,如果是,则不做任何事情。
- 增加该对象的引用计数,但是并不马上写回到 isa 变量中。
- 检查增加后的引用计数的值是否能够被19位表示,如果不是,则切换为以前的办法,否则执行第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
的数据结构定义,如下图:
对应的结构体定义如下:
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个部分:
-
isa
指针,所有对象都有该指针,用于实现对象的相关的功能。 -
flags
用于按bit
位表示一些block
的附加信息,在后面介绍的block copy
的实现代码中可以看到该变量的使用。 -
reserved
保留变量。 -
invoke
函数指针,指向具体的block
实现的函数调用地址。 -
descriptor
表示该block
的附加描述信息,主要是size
的大小,以及copy
和 dispose 函数的指针。 -
variable
capture
过来的变量,block 能够访问它外部的局部变量,就是因为将这些变量(或变量的地址)复制到了结构体中。
3.2 分类:
block
的类型:
_nsconcreteglobalblock
全局的静态block
,不会访问任何外部变量。_nsconcretestackblock
保存在栈中的block
,当函数返回时会被销毁。_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 的实现,从中我们可以看出:
- 一个
block
实际是一个对象,它主要由一个isa
、一个impl
和一个descriptor
组成。 - 由于这里没有开启
arc
,所以我们看到isa
的指向还是_nsconcretestackblock
。但在开启arc
时,block
应该是_nsconcreteglobalblock
类。 -
impl
是实际函数指针,本例中,它指向__main_block_func_0
。这里的impl
相当于之前提到的invoke
变量,只是clang
编译器对变量的命名不一样而已。 -
descriptor
是用于描述当前这个block
的附加信息的,包括结构体的大小,需要capture
和dispose
的变量列表等。
结构体大小需要保存到原因是,每个block
会capture
一些变量,这里变量会加到__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; }
在本例中,我们可以看到:
- 本例中,
isa
指向_nsconcretestackblock
,说明这是一个分配在栈上的实例。 -
__main_block_impl_0
中增加一个变量a
,在block
中引用的变量a
,实际是在声明block
时,被复制到__main_block_impl_0
结构体中的那个变量a
。 -
__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; }
从代码中我们可以看到:
- 源码中增加了一个名为
__block_byref_i_0
的结构体,用于保存我们要capture
并且修改的变量i
。 -
__main_block_impl_0
中引用的是__block_byref_i_0
的结构体指针,这样就可以起到修改外部变量的作用。 -
__block_byref_i_0
的结构体带有isa
,说明它也是一个对象。 - 我们需要负责
__block_byref_i_0
结构体相关的内存管理,所有__main_block_desc_0
中增加了copy
和dispose
函数指针,用于在调用前后修改相应变量的引用计数。
具体文件见: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。