一、概述
Objective-C语言是一门动态语言,它将很多静态语言在编译和链接期所做的事推迟到运行时处理。这种动态语言的优势在于:写代码更加灵活性,比如可以把消息重定向到别的对象,可以动态替换一个方法的实现等。动态特性决定了Objective-C不仅需要一个编译器,还需要一个运行时系统(Runtime System)来执行编译的代码。这就是Objective-C Runtime系统存在的意义,它是整个Objective-C运行框架的基石,Runtime System由Runtime库实现,该库基本上是用C和汇编语言写的,它使C语言有了面向对象的能力。
Objective-C Runtime有两个版本: 一个 Modern Runtime 和一个 Legacy Runtime。Modern Runtime(Objective-C 2.0)只能运行在 iOS 和Mac OS X 10.5 之后的64位程序中,Legacy Runtime(Objective-C 1.0) 运行在早期32位 Mac OS X的程序中,目前可以忽略该版本。
Objective-C Runtime系统是一个由一些列C函数和数据结构组成,具有公共接口的动态库。通过#import<objc/runtime.h>或者@import ObjectiveC引入Runtime模块,使用其中的API。开发人员基本上不用跟Runtime直接打交道,编译器在背后默默地完成大部分工作,另外NSObject封装了部分接口:isKindOfClass: isMemberOfClass: methodForSelector: respondsToSelector: conformsToProtocol等等。
iOS初学开发者往往把时间花在Cocoa框架和应用UI上,而忽视Objective-C的底层实现。但作为一个iOS开发者必须了解和掌握Runtime, 特别是对越狱开发者来说,必须深入学习、理解和应用相关特性。
二、主要数据结构
2.1Class类
Class的定义在objc.h中:
typedef struct objc_class *Class;
objc_class的定义在runtime.h中:
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
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;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif
} OBJC2_UNAVAILABLE;
Objective-C 2.0以后上述结构体可以简化为:
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;
}
上述头文件中的定义只是保证了编译通过,实际的实现细节被隐藏了, objc_class具体实现查看源代码如下(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
};
struct objc_object {
private:
isa_t isa;
};
union isa_t {
Class cls;
uintptr_t bits;
struct bitmap;
};
注:关于Class定义公开API中的实现和真正的实现有些出入,但是基本的元素是一样的,不妨碍具体分析。后面涉及的数据结构时会先列出在公开API中的定义,然后把真正的实现列出来分析。
上述定义是Objective-C比较新的实现,结构体中的函数已省略,其中superclass比较直观,指向父类,实现继承关系。struct bitmap的定义在Arm64和x86 64位下是不同的,为方便表述用bitmap代替,可以看出,objc_class 继承之objc_object,说明Class本身也是一个对象,objc_object唯一的成员isa是一个联合体(Union)isa_t,可以代表cls、位字段、bitmap。之所以这么实现,主要是因为针对64位的性能优化,原先版本的实现,isa是一个Class,指向元类(Meta Class),但在64位架构下指针 往往用不着64位, 这些闲置位可以利用,用来标记一些属性,提高访问性能。
Arm64下,isa的mask定义如下,使用了33位,其实只用了30位,因为整个结构体是字节对齐的,最后3位也可另作他用。
# define ISA_MASK 0x00000001fffffff8ULL
x86 86位下,isa的mask定义如下,使用了47位
# define ISA_MASK 0x00007ffffffffff8ULL
以Arm64位为例,struct bitmap的定义如下:
struct {
uintptr_t indexed : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 30;
uintptr_t magic : 9;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 19;
};
has_assoc标记是否有associated对象(通过objc_setAssocicatedObject设置),has_cxx_dtor标记是否有C++的析构函数,magic是固定标记值(3fe),weakly_referenced标记是否有对象有__weak引用,has_sidetable_rc标记是否有记录retain count的HashTable,extra_rc记录retain count。Extra_rc占19位,当retain count在19位中无法放下时,才会使用HashTable来存储retain count。
注:Xcode4.6以后isa已经不能直接使用,直接使用编译器会报错。
Class中的bits字段比较重要,表面上看它是用来保存一些标记位的,但事实上它和isa一样,里面有一段是用来保存指向类数据的指针,包括方法,属性,Category等信息都放在data数据中。cache主要是优化用,访问过的方法等会保存在cache中,下次访问时会先在 cache中查找。
2.2 Ivar 变量
Ivar的定义在runtime.h中
typedef struct objc_ivar *Ivar;
struct objc_ivar {
char *ivar_name OBJC2_UNAVAILABLE;
char *ivar_type OBJC2_UNAVAILABLE;
int ivar_offset OBJC2_UNAVAILABLE;
#ifdef __LP64__
int spac OBJC2_UNAVAILABLE;
#endif
}
Iva具体实现查看源代码如下(objc-runtime-new.h):
struct ivar_t {
int32_t *offset;
const char *name;
const char *type;
uint32_t alignmen;
uint32_t size;
};
2.3 Method 方法
Method的定义在runtime.h中
typedef struct objc_method *Method;
struct objc_method {
SEL method_name OBJC2_UNAVAILABLE;
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE;
}
Method具体实现查看源代码如下(objc-runtime-new.h):
struct method_t {
SEL name;
const char *types;
IMP imp;
};
name为方法名,类型为SEL
SEL的定义在objc.h中
typedef struct objc_selector *SEL;
objc_selector在源码中未找到对应的定义,但是从sel_getName的实现可以看出其实就是const char *。
const char *sel_getName(SEL sel) {
return (const char *)(const void*)sel;
}
但是在具体使用时,不能直接把const char*转成SEL使用,因为Runtime底下SEL的比较是基于内存地址的,因此转换后的SEL地址和Runtime中记录的不匹配,会导致找不到对应的IMP,直接Crash掉,因此使用时还是要调用sel_getUID或者sel_registerName返回SEL。
types可以理解为该方法的函数签名(TypeEncdoing),例如UIView的initWithFrame:方法的Type Encoding会表示成如下形式:(64位架构下)
@48@0:8{CGRect={CGPoint=dd}{CGSize=dd}}
@表示返回值是id类型
48表示参数栈大小位48个字节
@0表示第一个参数(self参数)是id类型,偏移量为0
:8表示第二个参数(_cmd参数)是SEL类型,偏移量为8
{CGRect={CGPoint=dd}{CGSize=dd}}表示第三个参数为t结构体,结构体名CGRect
imp为方法的实现,类型为IMP
IMP的定义在objc.h中
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id (*IMP)(id, SEL, ...);
#endif
可以把IMP理解为函数指针,如果要直接使用IMP来调用函数,原先可以直接通过变参的方式调用,目前Objective C中不允许直接调用,使用前必须把它转换成对应的函数签名,否则调用会出错,比如上面提到的initWithFrame:需要这样调用
((id (*)(id, SEL, CGRect)imp)(self,selctor,frame);
2.4 Property 属性
Property的定义在runtime.h中
typedef struct objc_property *objc_property_t;
Property具体实现查看源代码如下(objc-runtime-new.h):
struct property_t {
const char *name;
const char *attributes;
};
name就是Propety的名字,比较直观,不作介绍。 attributes是Property的属性,它是一个字符串表示的属性列表,以逗号分隔,每一项为一个属性,格式如下:T<type encoding>,….,V<property name>,以T开头,紧跟property类型的Type Encoding,以V结尾,紧跟变量名,中间是以「,」分隔的描述符,具体如下表所示:
First Header | Second Header |
---|---|
R | The propetty is read only - readonly |
C | The propetty is a copy of the value last assigned - copy |
& | The propetty is a reference to the value last assigned - retain |
N | The propetty is non-atomic - nonatomic |
G<name> | The propetty defines a custom getter selector name – getter= |
S<name> | The propetty defines a custom setter selector name – setter= |
D | The propetty is dynamic = dynamic |
W | The propetty is a weak reference = weak |
P | The propetty is a strong reference = strong |
比如以UIview的layer property为例:
@property(nonatomic,readonly,retain)CALayer *layer;
name:layer
attributes:T@”CALayer”,R,&,N,V_layer
T@”CALayer”表示返回CALayer对象
R,&,N表示readonly,nonatiomic,retain
V_layer表示UIView类中用_layer变量
Runtime中提供了API copyAttributeList可以获取属性列表,返回值为一个列表,每一项是一个结构体,定义如下:
typedef struct {
const char *name; /**< The name of the attribute */
const char *value; /**< The value of the attribute*/
} objc_property_attribute_t;
其中name的值为上述表中的值,外加T和V;value值通常都是NULL,T和V都有对应的值,另外S(setter)和G(getter)也有对应的值。
2.5 Category 类别
Category的定义在runtime.h中
typedef struct objc_category *Category;
struct objc_category {
char *category_name OBJC2_UNAVAILABLE;
char *class_name OBJC2_UNAVAILABLE;
struct objc_method_list *instance_methods OBJC2_UNAVAILABLE;
struct objc_method_list *class_methods OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
}
Category具体实现查看源代码如下(objc-runtime-new.h):
struct category_t {
const char *name;
classref_t cls;
struct method_list_t *instanceMethods;
struct method_list_t *classMethods;
struct protocol_list_t *protocols;
struct property_list_t *instanceProperties;
};
具体的字段比较直观,不多做进一步的分析,这里的name是类名,不是Category的名字。Runtmie中Category对用户是透明,Category中的方法和主类中的方法对用户来说是一样的。因此你无法知道Class中有多少个Category,哪些方法是属于Category的。
2.6 Protocol
Protocol的定义在runtime.h中
#ifdef __OBJC__
@class Protocol;
#else
typedef struct objc_object Protocol;
#endif
Protocol具体实现查看源代码如下(objc-runtime-new.h):
struct protocol_t : objc_object {
const char *mangledName;
struct protocol_list_t *protocols;
method_list_t *instanceMethods;
method_list_t *classMethods;
method_list_t *optionalInstanceMethods;
method_list_t *optionalClassMethods;
property_list_t *instanceProperties;
uint32_t size;
uint32_t flags;
const char **extendedMethodTypes;
const char *_demangledName;
};
具体的字段比较直观,这里就不展开了。
2.7 id & super
id的定义在objc.h中
typedef struct objc_object *id;
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
id就是一个指向objc_object的指针,另外,NSObject中也有一个成员变量isa,这样就可以用id指向任何NSObject的对象了。
super是编译器的关键字,当编译器碰到super关键字时,会把它替换成obj_super,obj_super的定义如下:
struct objc_super {
__unsafe_unretained id receiver;
__unsafe_unretained Class super_class;
};
2.8 List - Runtime中的列表结构(method_list_t, property_list_t, protocol_list_t, ivar_list_t)
List结构在Runtime公开API中是看不到的,主要是源码中出镜率很高,因此这里作一个简单的介绍,这些列表结构都是类似的实现,因此下面以ivar_list_t为例说明。
老的实现为:
struct old_ivar_list {
int ivar_count;
struct old_ivar ivar_list[1];
};
新的实现为;
struct ivar_list_t {
uint32_t entsize;
uint32_t count;
ivar_t first;
};
两者其实是一样的,新的实现中要访问ivar列表时,需要先取first的地址,再加偏移量。可以这么理解:整个ivar列表是一块连续的内存,Runtime知道一个类有多少个成员变量ivar,就可以知道整个列表的空间大小=sizeof(ivar_list_t) + (count - 1) * entsize; 当然如果count = 0,那么就是sizeof(ivar_list_t) 。分配好空间后,第一个ivar可以直接通过first访问,其它ivar需要先取first的地址,再加上偏移量访问了。
三、类 (Class)
类在数据结构一节中简单提到过,包括用superClass来实现继承,NSObject的定义等。这里再对它做一个比较详细的讲述。主要包括类的层级,类变量布局,以及一些类的基本操作。
3.1 类层级(Class Hierarchy)
在数据结构一节中介绍了类的结构体,这里进一步分析类的层次关系,主要涉及到几个概念。实例对象:即类的实例化对象;元类metaclass:即类的类信息;根类root class:可理解为NSObject类。首先分析一下,类、父类、根类、元类(metaclass)的isa和super_class之间的关系。 具体如下图:
总结如下:
- 类的实例对象的isa指向该类
- 类的isa指向该类的meta class
- 类的super_class指向父类,如果该类为根类则为NULL
- metaclass的isa指向根metaclass,如果该metaclass为根metaclass则指向自己
- metaclass的super_class 指向父metaclass,如果该metaclass为根metaclass则指向自己。
总得来说,一个对象有两个Class与之对应,一个是普通的Class,一个是mata class。访问实例方法时,会遍历普通的Class,访问类方法时,需要遍历meta class。metaclass的继承关系和普通的类的继承关系是一一对应的。
3.2类布局(Class Layout)
当实例化一个类时,Runtime会根据类中的信息计算实例的大小,并分配内存空间,类中Ivar的布局可以参考Ivar一节中的图例。第一个字段总是isa,指向对象的类信息,然后依次存放成员变量,基类的放在前面,子类的放在后面。当需要访问成员变量时,会根据类中的信息,计算偏移,操作内存地址,设置和读取变量值。这个我们可以通过简单的示例看一下Runtime中如何访问成员变量的,比如定义一个IntergerNumber的类:
@interface IntergerNumber : NSObject {
NSInteger _value;
}
- (NSInteger)intValue;
@end
@implementation IntergerNumber
- (NSInteger)intValue {
return _value;
}
@end
IntergerNumber类并没有提供修改_value值的方法,但可以通过偏移量直接修改。
Ivar var = class_getInstanceVariable([IntergerNumber class], "_value");
ptrdiff_t offset = ivar_getOffset(var);
*((NSInteger *)((char *)(__bridge void *)number + offset)) = 2048;
这里只是为了说明布局问题,Runtime提供了修改Ivar的API,
3.3类加载和初始化
关于Runtime如何实现类的初始化和加载过程,可参考后面Category加载中的分析。这里主要分析NSObject中的两个类方法+load & + initialize。
+load 在类加载的时候调用,如果App或者App链接的Framework中的类实现了该方法,+load会在main方法前调用;如果在可加载运行的bundle中实现该方法,在bundle加载过程中调用。+load调用的时间比较早,因此需要特别小心,需要考虑+load是否引用了其它类,因为你无法确认该类是否加载完成;另外,Autorelease pool在这个时候还不存在,需要特别注意内存管理。
注:如果主类和Category中都实现了+load,那么两者都会被调用,因此+load中比较适合做一些”邪恶”的事情,比如Method Swizzle。+ initialize相对于+load来说调用的时机比较靠后,甚至有可能根本就不调用,因此相对比较适合添加一些trick的代码。+ initialize在类加载时不会被调用,而是在第一次给类发消息时,触发调用的,并且每个类只会调用一次,下面是比较直观的伪代码:
id objc_msgSend(id self, SEL _cmd, ...) {
if(!self->class->initialized)
[self->class initialize];
...send the message...
}
当然,实际的实现要比上面复杂,需要考虑线程安全等问题,但是基本的原理是一样的。实际使用+ initialize时也要考虑安全性,尽管它一般是在main函数后调用的,不需要考虑与其它类之间的依赖问题,但也因为它是推迟执行的,它不适合做一些注册类的事情,另外,如果在基类中实现了+ initialize,子类中没实现,那么子类调用+ initialize时,就会调用父类的+ initialize,导致+ initialize有可能会调用两次。所以+ initialize通常需要实现成以下形式:
+ (void)initialize {
if(self == [XXXClass class]) {
...perform initialization...
}
}
以下是Apple文档中类加载和初始化顺序的描述:
- All classes from frameworks you link to are loaded and initialized.
- All classes from your code (bundle) are loaded.
- All C++ static initializers and C/C++ 4. attribute(constructor) functions in your bundle are called.
All initializers in frameworks that link to your code.
3.4类操作方法
这里只介绍几个常用的类操作方法:
1. object_getClass
Class object_getClass(id obj) {
if (obj) return obj->getIsa();
else return Nil;
}
object_getClass返回isa,如果对象本身是Class,isa则会返回Class的metaclas
2. object_isClass
BOOL object_isClass(id obj) {
if (!obj) return NO;
return obj->isClass();
}
inline boolobjc_object::isClass() {
return ISA()->isMetaClass();
}
通过判断isa是不是metaclass来判断对象是否为Class,如果是Class则它的isa中metaclass标记为true。因此object_isClass也可以通过组合调用class_isMetaClass(object_getClass(obj))来实现.
其它获取method、ivar、property、protocol的实现,就不一一列举了。
四、消息传递(Messaging)
Objective C中向一个对象发送消息是通过objc_msgSend函数实现的,编译器会把[receiver message]转换成objc_msgSend(receiver,selector)。
objc_msgSend的定义在message.h中
#if !OBJC_OLD_DISPATCH_PROTOTYPES
void objc_msgSend(void /* id self, SEL op, ... */ );
#else
id objc_msgSend(id self, SEL op, ...);
#endif
新的声明方式objc_msgSend没有参数和返回值,在使用前必须根据实际情况强转类型,否则调用会出错。老的声明是一个变参函数,更容易理解,第一个参数是self(对象或者类),第二个参数是_cmd对应方法的selector,后面是方法的参数,前面2个参数在源代码中是隐藏的,即Objective C中每个方法都有这两个参数。 objc_msgSend实际上是用汇编语言实现的(必须用汇编实现,c函数无法做到一个变参函数处理所有参数和返回值的情况),并且不同的CPU架构有不同的实现,根据返回值不同,也有不同的实现。汇编语言可读性比较差,下面以伪代码的形式来分析objc_msgSend函数。
id objc_msgSend(id self, SEL _cmd, ...) {
Class cls = object_getClass(self);
IMP imp = cache_lookup(cls, _cmd);
if(!imp) {
imp = class_getMethodImplementation(cls, _cmd);
}
return imp(self, _cmd, ...);
}
上面是objc_msgSend用C语言实现的 伪代码。首先从Cache中找方法的实现IMP,如果无法找到,接着调用class_getMethodImplementation获取方法的IMP,最后调用对应的实现。class_getMethodImplementation实现如下:
上面是objc_msgSend用C语言实现的 伪代码。首先从Cache中找方法的实现IMP,如果无法找到,接着调用class_getMethodImplementation获取方法的IMP,最后调用对应的实现。class_getMethodImplementation实现如下:
IMP class_getMethodImplementation(Class cls, SEL sel) {
IMP imp;
if (!cls || !sel) return nil;
imp = lookUpImpOrNil(cls, sel, nil, YES,YES,YES);
if (!imp) {
return _objc_msgForward;
}
return imp;
}
其中,lookUpImpOrNil会遍历类中的方法,如果找不到,会继续查找父类中是否有对应的实现,如果遍历到根类也无法找到方法的实现,则返回nil。如果lookUpImpOrNil返回nil,最后会返回_objc_msgForward。
这里涉及到两个概念:方法动态决定(Dynamic Method Resolution)和消息转发。
方法动态决定(Dynamic Method Resolution)在lookUpImpOrNil中实现的,代码片段如下:
IMP lookUpImpOrForward(Class cls, SEL sel, id inst, bool initialize, bool cache, bool resolver) {
// Omited Source Code
// No implementation found. Try method resolver once.
if (resolver && !triedResolver) {
mutex_unlock(&methodListLock);
_class_resolveMethod(cls, sel, inst);
triedResolver = YES;
goto retry;
}
// Omited Source Code
}
void _class_resolveMethod(Class cls, SEL sel, id inst) {
if (! cls->isMetaClass()) {
_class_resolveInstanceMethod(cls, sel, inst);
} else {
_class_resolveClassMethod(cls, sel, inst);
if (!lookUpImpOrNil(cls, sel, inst, NO, YES, NO )) {
_class_resolveInstanceMethod(cls, sel, inst);
}
}
}
方法动态决定(Dynamic Method Resolution)实际上就是Runtime给我们一次处理异常的机会,当无法在类中找到对应的方法实现时,会调用NSObject的以下方法:
+ (BOOL)resolveClassMethod:(SEL)sel
+ (BOOL)resolveInstanceMethod:(SEL)sel
可以在上述方法中,添加类的方法实现,告诉Runtime可以处理该消息。注意:上述方法的返回值不影响最后的决议,但是为了保持逻辑正确性,还是根据实际的情况返回对应的值。
消息转发是通过_objc_msgForward实现的,如果在类中无法找到对应实现,并且也没有在方法动态决定(Dynamic Method Resolution)中处理,那么Runtime就会调用_objc_msgForward 方法。主要有两个过程:
1.快速转发路径(Fast Forwarding Path)
Runtime首先会询问是否需要将此消息原封不动地转发给其它对象,这是比较常见的转发场景,也是开销比较小的实现。只需要在类中实现forwardTargetForSelector:并且返回非nil和self的值,该消息就会发送到对应的对象上。
- (id)forwardingTargetForSelector:(SEL)sel {
return object;
}
2.常规转发路径 (Normal forwarding Path)
如果前面的条件都无法满足,Runtime最后会创建一个NSInvocatin对象,创建前会调用- (NSMethodSignature *)methodSignatureForSelector:(SEL)方法获取NSMethodSignature对象, 一旦NSInvocation创建完成,就会调用forwardInvocation:方法。实现消息转发,需要在子类中实现该方法,因为默认的实现只是简单地调用- (void)doesNotRecognizeSelector:(SEL)sel方法,该方法什么也不做,直接Crash掉。
整体流程图如下:
五、方法替换 (Method Swizzle)
方法替换就是改变(替换)类中已存在的selector的原本实现,这个功能是基于Objective C Runtim在运行时再去绑实现的,Objective C中对一个对象发送消息,消息对应的实现IMP是可以动态修改的。
实现Method Swizzle的最简单方法就是调用Runtime中的API
void method_exchangeImplementations(Method m1, Method m2)
@implementation ViewController {
+ (void)load {
if (self == [ViewController class]) {
Method method1 = class_getInstanceMethod(self, @selector(viewDidLoad));
Method method2 = class_getInstanceMethod(self, @selector(swizzleViewDidLoad));
method_exchangeImplementations(method1, method2);
}
}
- (void)swizzleViewDidLoad {
}
}
但是这种实现方式比较粗暴,并且不是很安全,比如子类中并未实现对应的selector,那么基类中的实现就会被替换,通常这不是预期的;另一方面,如果方法实现中依赖_cmd参数,因为传入的cmd值不对,调用很可能会出错。
正确的方法应该通过调用Runtime中另外两个API:
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
IMP method_setImplementation(Method m, IMP imp)
具体实现如下:
void viewDidLoadImp(id self, SEL _cmd) {
}
+ (void)load {
if (self == [ViewController class]) {
Method method = class_getInstanceMethod(self, @selector(viewDidLoad));
if(!class_addMethod(self,
@selector(viewDidLoad),
(IMP)viewDidLoadImp,
method_getTypeEncoding(method))) {
method_setImplementation(method, (IMP)viewDidLoadImp);
}
}
}
class_addMethod如果成功,说明类中没有该方法的实现,就会添加一个新的方法,如果失败,说明已存在方法的实现,就把原先的实现替换成新的实现。这样就可以解决上面例子中提到的_cmd和基类两个问题。
上面简单介绍了如何实现Methold Swizzle,在实际使用中还需要考虑保留原有实现,然后回调原有的实现;考虑第三方多次Methold Swizzle后保证程序正常运行;另外应该尽可能放在+load方法中初始化。
六、Category
6.1 Category 编译过程
首先分析一下Category的编译过程,用简单的例子来分析,如下实现一个 Category:
@interface IntegerNumber(Test)
- (void)dump;
@end
@implementation IntegerNumber(Test)
- (void)dump {
}
@end
然后在终端运行clang -rewrite-objc IntegerNumber.m 会产生IntegerNumber.cpp文件(objc 重写成c++),文件比较复杂,主要是import进来的文件产生的代码,本身的实现可以在文件尾部找到,上述Category会转换成如下结构:
static struct _category_t _OBJC_$_CATEGORY_IntegerNumber_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
"IntegerNumber",
0, // &OBJC_CLASS_$_IntegerNumber,
(const struct _method_list_t *)&_OBJC_$_CATEGORY_INSTANCE_METHODS_IntegerNumber_$_Test,
0,
0,
0,
};
从OBJC$CATEGORY_IntegerNumber$_Test的命名中可以得知类名和Category名分别为IntegerNumber和Test。因为在Category中只有一个实例方法,因此只有OBJC$CATEGORY_INSTANCE_METHODS_IntegerNumber$_Test这个实例方法列表。然后找到该方法列表的定义如下:
static struct /*_method_list_t*/ {
unsigned int entsize; // sizeof(struct _objc_method)
unsigned int method_count;
struct _objc_method method_list[1];
} _OBJC_$_CATEGORY_INSTANCE_METHODS_IntegerNumber_$_Test __attribute__ ((used, section ("__DATA,__objc_const"))) = {
sizeof(_objc_method),
1,
{{(struct objc_selector *)"dump", "v16@0:8", (void *)_I_IntegerNumber_Test_dump}}
};
方法列表里面只有一个方法void *)_I_IntegerNumber_Test_dump,其中I表示实例方法。另外还有方法名和方法的签名(其实这就是Method数据结构)。上述数据都是放在__DATA,__objc_const section区域。最后,这个类会生成一个数组,存放在__DATA, __objc_catlist section区域。
static struct _category_t *L_OBJC_LABEL_CATEGORY_$ [1] __attribute__((used, section ("__DATA, __objc_catlist,regular,no_dead_strip")))= {
&_OBJC_$_CATEGORY_IntegerNumber_$_Test,
};
6.2 Category 加载过程
Category编译后的信息不会保存到原始的类结构中,它是Runtime后期加载的。下面简单分析一下Runtime源代码,看看Category是如何加载的。首先,Runtime加载的入口函数_objc_init方法(objc-os.mm),在library加载前由libSystem.dyld调用,进行初始化操作:
void _objc_init(void) {
static bool initialized = false;
if (initialized) return;
initialized = true;
// Register for unmap first, in case some +load unmaps something
_dyld_register_func_for_remove_image(&unmap_image);
dyld_register_image_state_change_handler(dyld_image_state_bound,
1/*batch*/, &map_images);
dyld_register_image_state_change_handler(dyld_image_state_dependents_initialized, 0/*not batch*/, &load_images);
}
主要工作注册几个回调函数,真正的加载函数是map_images,具体的实现这里就不分析了,主要工作就是把镜像(image)map到内存中,然后调用_read_images方法。_read_images方法干了很多苦力活,比如加载类,Protocol,Category等,其中加载Category部分的代码如下:
category_t **catlist = _getObjc2CategoryList(hi, &count);
for (i = 0; i < count; i++) {
category_t *cat = catlist[i];
Class cls = remapClass(cat->cls);
if (cat->instanceMethods || cat->protocols
|| cat->instanceProperties) {
addUnattachedCategoryForClass(cat, cls, hi);
if (cls->isRealized()) {
remethodizeClass(cls);
}
}
if (cat->classMethods || cat->protocols ) {
addUnattachedCategoryForClass(cat, cls->ISA(),hi);
if (cls->isRealized()) {
remethodizeClass(cls);
}
}
}
源码中部分实现已去除,保留了核心逻辑,大致步骤如下:
- 通过_getObjc2CategoryList加载所有Category List,其定义如下,实际工作就是把编译阶段产生的
"__objc_catlist" section中的Category列表加载进来。GETSECT(_getObjc2CategoryList, category_t *, "__objc_catlist");
- 遍历Category List中的每个Category,如果有实例方法,把Category中的实例方法加到class中,如果有类方法,把Category中的类方法加到meta class中。其中,addUnattachedCategoryForClass只是将Category注册到对应的类,如果类还没有初始化完,后面就不会加到类的方法列表中,等类初始化完会再处理Category方法的的加载。 remethodizeClass方法把Category中的方法列表加载到类中,通过调用attachCategoryMethods方法实现。
static void
attachCategoryMethods(Class cls, category_list *cats, bool flushCaches) {
if (!cats) return;
if (PrintReplacedMethods) printReplacements(cls, cats);
bool isMeta = cls->isMetaClass();
method_list_t **mlists = (method_list_t **)
_malloc_internal(cats->count * sizeof(*mlists));
// Count backwards through cats to get newest categories first
int mcount = 0;
int i = cats->count;
BOOL fromBundle = NO;
while (i--) {
method_list_t *mlist = cat_method_list(cats->list[i].cat, isMeta);
if (mlist) {
mlists[mcount++] = mlist;
fromBundle |= cats->list[i].fromBundle;
}
}
attachMethodLists(cls, mlists, mcount, NO, fromBundle, flushCaches);
_free_internal(mlists);
}
该方法把Categroy List中的所有Method List取出来,调用attachMethodLists加到对应的类中。不过添加过程是逆序的,保证新的实现在老的实现前面。另外,这些Method List插入到类中时,插入到原先Method List的前面,因此新的实现会覆盖类中原先的实现。
分析了Category的加载过程,我们会发现,如果有两个Category存在同名的方法,或者Category中实现了主类已存在的方法,那么就会出现方法覆盖的问题,因此除非必要,否则尽量避免使用Category,如果非得使用,也要为其中的方法,添加前缀或者后缀,以防方法冲突。
6.3 Class Extension & Instance Property
Class Extension可以认为是特殊的Category——没有名字Category,它们的主要区别:Class Extension必须要有源代码,在Class Extension中声明的方法,必须在implement文件中实现,因此无法为Framework中的类添加Extension,而Category不依赖源文件,可以为任何类添加方法;Class Extension和Category中都可以声明property,但是Class Extension中的property,编译器会为它合成accessor方法,同时会合成成员变量,而Category中的property编译器不会为它生成accessor方法,更不用说成员变量了;另外在Class Extentsion中声明的property,可以在implemnt文件中修改property的修饰属性,比如readonly的property可以重写为readwrite的。
前面分提到Category中声明的Property编译器不会自动生成accesor方法和成员变量,但是可以通过Runtime中提供的Associated Object来实现(模拟)Instance property。Associated Object是OSX 10.6 & iOS 3.1后新加的特性,即每一个对象都可以拥有一个Dictionary来存储一些额外的key/value对。下面以NSObject添加一个tag的Property为例子说明:
首先在NSString Category头文件中声明tag Property
@interface NSString (TaggedString)
@property (nonatomic, copy) NSString *tag;
@end
下面是具体实现:
static const char *kTagKey = "StringTagKey";
@implementation NSString (Tag)
- (NSString *)tag {
return objc_getAssociatedObject(self, kTagKey);
}
- (void)setTag:(NSString *)tag {
objc_setAssociatedObject(self, kTagKey, tag, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
@end
其中需要注意的是objc_getAssociatedObject和objc_setAssociatedObject中使用的key是void *类型,不是NSString类型,因此在使用中,必须传递同一个指针,一般声明一个static的常量(任意类型),然后再定义一个指针指向它,作为key。
objc_setAssociatedObject中除了
OBJC_ASSOCIATION_COPY_NONATOMICOBJC_ASSOCIATION_COPY、OBJC_ASSOCIATION_RETAIN_NONATOMIC、OBJC_ASSOCIATION_RETAIN、OBJC_ASSOCIATION_ASSIGN。