iOS RunTime之一:基础 - 数据结构

参考:
Objective-C Runtime Programming Guide
深入Objective-C的动态特性
Objective-C Runtime 运行时之一六:类与对象拾遗
刨根问底Objective-C Runtime 系列
Objective-C Runtime 1小时入门教程
Objective-C 与 Runtime
Objective-C Runtime源码传送门


一个剖析runtime的利器:

$ clang -rewrite-objc 源文件名(比如test.m)

引子

Runtime到底是什么?

Objective-C,顾名思义,和C++一样都是在C的基础上加入面向对象的特性扩充而成的程序设计语言,但二者实现的机制差异很大(C++是基于静态编译时类型,而Objective-C是基于动态运行时类型)。关键就在于OC的runtime, runtime是一个用C和汇编编写的动态库,它将OC和C紧密关联并提供动态特性(动态类型、动态绑定函数实现、动态加载资源),这个系统主要做两件事 :
1、封装C语言的结构体和函数,让开发者在运行时创建、检查或者修改类、对象和方法等等。
  ◈ Class ( objc_class * ) => objc_objectid ( objc_object * )、objc_super
  ◈ Ivarobjc_property_t ( objc_property_attribute_t )
  ◈ objc_method_description ( 即SEL + types ) + IMP + => Method ( objc_method * )
  ◈ ……
  ◈ Objective-C Runtime Reference
2、传递消息,找出方法的最终执行代码。
  ◈ 静态类型编程语言(比如C++)的函数调用和OC的方法调用(消息发送)的区别在于前者在编译期就确定了(函数的地址),而后者是运行时动态确定(代价是性能下降,objc_class中的objc_cache就是用来补偿这种性能下降的);
  ◈ 类层次体系查找 ( isa+objc_method_list ) + 消息转发 ( 动态解析 => 备用接收者 => ( 签名+打包+完整转发 )

动态加载NSBundle类提供了许多面向对象的便捷接口用于动态加载;比如Retina设备自动加载@2x的图片。

应用场景举例

1、遍历对象的成员变量/属性
比如:自动实现<NSCoping>、<NSMutableCopying>、<NSCoding>协议(参考 Mantle 的源码实现)

2、动态添加/修改属性或成员变量,动态添加/修改/替换方法

3、动态创建类/对象/协议等等
比如:Model与Json Dictionary的相互转换(还涉及对象成员变量/属性的遍历),参考 MJExtension 的实现

4、方法拦截调用
比如:拦截imageNamed:、viewDidLoad、viewWillAppear:(统计页面展现次数等)、alloc。

5、为分类添加的属性提供动态的存取方法实现


传送门:RunTime源码

1. 基础数据类型

#类、对象、id以及协议

关注Class定义和isa、super_class、cache和version字段

// 类
struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;  //指向metaClass(元类)

#if !__OBJC2__
    Class super_class                       OBJC2_UNAVAILABLE;  // 父类
    const char *name                        OBJC2_UNAVAILABLE;  // 类名
    long version                            OBJC2_UNAVAILABLE;  // 类的版本信息,默认为0 => 序列化支持
    long info                               OBJC2_UNAVAILABLE;  // 类信息,供运行期使用的一些位标识
    long instance_size                      OBJC2_UNAVAILABLE;  // 该类的实例变量大小
    struct objc_ivar_list *ivars            OBJC2_UNAVAILABLE;  // 该类的成员变量链表
    struct objc_method_list **methodLists   OBJC2_UNAVAILABLE;  // 方法定义的链表
    struct objc_cache *cache                OBJC2_UNAVAILABLE;  // 方法缓存;优化methodLists查找
    struct objc_protocol_list *protocols    OBJC2_UNAVAILABLE;  // 协议链表
#endif
} OBJC2_UNAVAILABLE;

typedef struct objc_class *Class;
// 对象
struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};
// id
typedef struct objc_object *id;
// 协议
typedef struct objc_object Protocol;
#分类
typedef struct objc_category *Category;
#元类(Meta Class)

类自身也是一个对象(元类的对象),调用类方法其实就是向类对象发送消息;区别:
 ◈ 向一个对象发送消息时,runtime会在对象所属类的方法列表中查找方法;
 ◈ 向一个类发送消息时,会在这个类的meta-class的方法列表中查找方法。

#成员变量、属性
  • 成员变量与属性的联系 - 属性声明的本质是根据指定规则生成对应的成员变量及其存取方法
  • 成员变量名:默认是_xxx;当然你也可以用语法糖@synthesize自行指定
  • 存取方法:包括原子性(锁机制)、读写权限、内存管理策略、存取方法名设定
  • 编码
  • 纯类型编码 - c、i、s、l、q、C、I、S、L、Q或f、d、B、V、*、@、#、:、[]、{}、()、b、^、?
^{example=@*i}  //example类型表示包含一个id对象、一个char *、一个int的结构体的指针
^^{example} //一个example指针的指针
  • 方法类型编码
对 - (BOOL)danceWith:(NSString *)people at:(char)place on:(int)count;
调用const char * method_getTypeEncoding ( Method m );
=> 返回值(BOOL) + 隐藏参数self(id) + 隐藏参数_cmd(SEL) + 第一个参数(NSString *) + 第二个参数(char) + 第三个参数(int)
=> ("B"+32) + ("@"+0) + (":"+8) + ("@"+16) + ("c"+24) + ("i"+28)
=> "B32@0:8@16c24i28"
  • 属性类型编码 - T<纯类型编码>、V<s>、R、C、&、N、G<s>、S<s>、D、W、P、t<s>
对 @property (nonatomic, readonly, retain) NSString* vName;
调用const char * property_getAttributes ( objc_property_t property );
=> 属性编码 + nonatomic + readonly + retain + 属性对应的成员变量
=> ("T" + "@\"NSString\"") + ("N" + "") + ("R" + "") + ("&" + "") + ("V" + "_vName")
=> "T@\"NSString\",R,&,N,V_vName"
  • 基础数据类型
// 成员变量 - objc_ivar的结构在iOS 9.0已经被隐藏,其封装了成员变量的名字和类型。
typedef struct objc_ivar *Ivar;
// 属性
typedef struct objc_property *objc_property_t;
 
// 属性的特性 - 注意:特性V对应的是成员变量名(可被@synthesize指定或自动生成加前缀‘_’),不是属性名
typedef struct {
  const char *name;           // 特性名 - 必选:T、V 可选:R、C、&、N、G、S、D、W、P、t
  const char *value;          // 特性值 - 只有name为TVGSt时value不为nil,且T用纯类型编码表示
} objc_property_attribute_t;
#方法
  • SEL - 本质上是一个根据方法名hash化了的KEY值,它的存在只是为了加快方法的查询速度。
  • IMP - IMP实际上是一个函数指针,指向方法实现的首地址。
// 方法选择器
typedef struct objc_selector *SEL;
// 方法实现 - 第一个参数是指向self的指针(如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针)
id (*IMP)(id, SEL, ...)
// 方法 - 将SEL和对应的实现IMP关联起来
struct objc_method {
    SEL method_name                     OBJC2_UNAVAILABLE;  // 方法名
    char *method_types                  OBJC2_UNAVAILABLE;  // 返回值和参数类型编码; 可用+signatureWithObjCTypes:封装成方法签名
    IMP method_imp                      OBJC2_UNAVAILABLE;  // 方法实现
}  

typedef struct objc_method *Method;
#图形化
OC对象的内存布局.png
OC的类继承体系结构

附:健壮的实例变量
由于超类实例变量的变更(增删改)导致OC对象的内存布局变化,将由系统通过Ivar对象(封装了基地址和偏移量)管理,我们需要做的仅仅是通过名字直接获取或通过Ivar间接获取实例变量的值,复杂的事就交给系统吧。


自动偏移

2. 操作函数

注:增改类的方法放在系列二中。

#类
// 获取所有已注册类 - 前6个函数的搜索范围都是已注册到运行时系统的类(objc_registerClassPair)。
int objc_getClassList ( Class *buffer, int bufferCount );
Class * objc_copyClassList ( unsigned int *outCount );

------------------------------------------------  // 功能类似,区别在于未找到Class的情况:
Class objc_getClass ( const char *name )       // 会调class handler callback???,并再次检查Class是否注册
Class objc_lookUpClass ( const char *name )    // 直接返回nil
Class objc_getRequiredClass ( const char *name ) // 会kill进程,程序crash

Class objc_getMetaClass ( const char *name ) 
Class class_getSuperclass ( Class cls );      // cls不需注册

-------------------------------------------
const char * class_getName ( Class cls );     // cls不需注册
size_t class_getInstanceSize ( Class cls );    // cls不需注册

BOOL class_isMetaClass( Class cls );        // 不注册就没有元类
#分类

Runtime并没有在 <objc/runtime.h> 头文件中提供针对分类的操作函数。因为分类中的信息最终会被合并到对应类中(也即对应的objc_class中),所以我们可以通过针对objc_class的操作函数来获取分类的信息。

#协议

注意:从未被任何类实现的协议不会被自动注册到运行时系统,当然你也可以objc_registerProtocol手动注册

// 获取已注册的协议
Protocol * objc_getProtocol ( const char *name );  
Protocol ** objc_copyProtocolList ( unsigned int *outCount ); 

-------------------------------------------
/* 属性 */
objc_property_t protocol_getProperty ( Protocol *proto, const char *name, BOOL isRequiredProperty, BOOL isInstanceProperty );
objc_property_t * protocol_copyPropertyList ( Protocol *proto, unsigned int *outCount );

// 方法 - 方法描述包含了方法名(比如"init")、返回值和参数的类型编码信息(比如"@16@0:8")
struct objc_method_description protocol_getMethodDescription ( Protocol *p, SEL aSel, BOOL isRequiredMethod, BOOL isInstanceMethod );
struct objc_method_description * protocol_copyMethodDescriptionList ( Protocol *p, BOOL isRequiredMethod, BOOL isInstanceMethod, unsigned int *outCount );

/* 与其他协议的关系 */
Protocol ** protocol_copyProtocolList ( Protocol *proto, unsigned int *outCount );
BOOL protocol_conformsToProtocol ( Protocol *proto, Protocol *other );

-------------------------------------------
const char * protocol_getName ( Protocol *p );
BOOL protocol_isEqual ( Protocol *proto, Protocol *other );

-------------------------------------------
BOOL class_conformsToProtocol ( Class cls, Protocol *protocol );
Protocol * class_copyProtocolList ( Class cls, unsigned int *outCount );
#对象
/* 根据成员变量的类型(OC对象/其他)用不同的方法获取其信息*/
id object_getIvar ( id obj, Ivar ivar );
Ivar object_getInstanceVariable ( id obj, const char *name, void **outValue );
void * object_getIndexedIvars ( id obj ); // 给定实例obj的extra bytes的指针,详见上文图形化

/* 针对对象的类进行操作 */
const char * object_getClassName ( id obj );
Class object_getClass ( id obj );
#版本
int class_getVersion ( Class cls );
void class_setVersion ( Class cls, int version );
#成员变量
// 获取信息
const char * ivar_getName ( Ivar v );
const char * ivar_getTypeEncoding ( Ivar v ); // 比如"@\"NSString\""
ptrdiff_t ivar_getOffset ( Ivar v );

// 获取
Ivar class_getInstanceVariable ( Class cls, const char *name );   // 实例成员变量
Ivar class_getClassVariable ( Class cls, const char *name );     // 类变量,如何用OC代码声明一个类变量???
Ivar * class_copyIvarList ( Class cls, unsigned int *outCount );  // 实例/类的成员变量列表
#属性 - 属性可以replace,纯成员变量不行。
const char * property_getName ( objc_property_t property ); // 返回属性名(区别于成员变量名)
const char * property_getAttributes ( objc_property_t property ); 
char * property_copyAttributeValue ( objc_property_t property, const char *attributeName ); 
objc_property_attribute_t * property_copyAttributeList ( objc_property_t property, unsigned int *outCount ); 

// 获取
objc_property_t class_getProperty ( Class cls, const char *name );
objc_property_t protocol_getProperty( Protocol *proto, const char *name, BOOL isRequiredProperty, BOOL isInstanceProperty )
=> objc_property_t * class_copyPropertyList ( Class cls, unsigned int *outCount );
=> objc_property_t * protocol_copyPropertyList( Protocol *proto, unsigned int *outCount );

// 属性列表
objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)

#SEL
SEL sel_registerName ( const char *str ); // 也可使用更方便的语法糖 @selector(…)
SEL sel_getUid ( const char *str );     // 实现等价于sel_registerName

const char * sel_getName ( SEL sel );

BOOL sel_isEqual ( SEL lhs, SEL rhs );   // 只要方法名相同就返回YES
#IMP
// 获取实现 - 注意:涵盖了消息转发机制!!!
IMP method_getImplementation ( Method m );
IMP class_getMethodImplementation ( Class cls, SEL name );
IMP class_getMethodImplementation_stret ( Class cls, SEL name );

// 设置、交换实现
=> IMP method_setImplementation ( Method m, IMP imp );
=> void method_exchangeImplementations ( Method m1, Method m2 );

// 使用方便的块语法
=> IMP imp_implementationWithBlock(id block)
=> id imp_getBlock( IMP anImp) 
=> BOOL imp_removeBlock( IMP anImp) 
#Method

注:要获取类方法列表,用class_copyMethodList(object_getClass(cls), &count)实现。

// 查
Method class_getInstanceMethod ( Class cls, SEL name );         // 搜索父类
Method class_getClassMethod ( Class cls, SEL name );           // 搜索父类
Method * class_copyMethodList ( Class cls, unsigned int *outCount ); // 不搜索父类, 只返回实例方法

SEL method_getName ( Method m );
unsigned int method_getNumberOfArguments ( Method m );
struct objc_method_description * method_getDescription ( Method m );

// 获取参数或/和返回值的类型编码,
const char * method_getTypeEncoding ( Method m );           // 比如"B32@0:8@16c24i28"
char * method_copyArgumentType ( Method m, unsigned int index );  // 0->"@", 1->":", 2->"c", 3->"i"
void method_getArgumentType ( Method m, unsigned int index, char *dst, size_t dst_len );
char * method_copyReturnType ( Method m );               // "B"
void method_getReturnType ( Method m, char *dst, size_t dst_len );

--------------------------------------------------------------------------------
// 方法调用
=> id method_invoke ( id receiver, Method m, ... );
=> void method_invoke_stret ( id receiver, Method m, ... ); // 调用返回一个数据结构的方法的实现
#关联对象

=> 解决了category不能添加成员变量()的问题 => 引申:为指定对象动态添加功能的解决方案

注意
1、在category中可以声明属性,但是不会自动生成成员变量和对应的存取方法;
2、关联对象并不会自动生成属性或成员变量,通常做法是声明一个属性,并利用关联对象动态实现其存取方法,使其看起来就像一个正常的属性一样。

// 5种内存管理策略: OBJC_ASSOCIATION_[ASSIGN/RETAIN/RETAIN_NONATOMIC/COPY/COPY_NONATOMIC]
// 也可以通过设置value为nil来移除关联
=> void objc_setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy) 

=> id objc_getAssociatedObject(id object, void *key) 

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

推荐阅读更多精彩内容

  • 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的转载 这篇文章完全是基于南峰子老师博客的...
    西木阅读 30,470评论 33 467
  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,643评论 0 9
  • 编程时常常需要把表达式的值赋给变量,这就要求在声明变量的时候清楚地知道表达式的类型。然而要做到这一点并非那么容易,...
    lgz00gi阅读 643评论 0 1
  • 绿荫里,虫子飞舞着,透过树叶的斑驳阳光,与草静谧相处着,蚂蚁搬起果实的残骸,一阵风吹来,让我不由得想起仰望天空,寻...
    野墨尘阅读 149评论 0 0
  • 今天恰好有时间,想写点之前很想写的东西,恰好看了吴晓波频道的视频(想看的可以点击这里),里面讲王石、冯仑他们的斜杠...
    笑自来阅读 342评论 0 2