文章目录
Runtime-源码分析
1.类的初始化 在外部是如何实现的?
2.初始化过程中runtime 起到了什么作用?
类的结构体
类是继承于对象的:
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();
}
......
}
在objc_class
中有定义了三个变量 ,superclass
是一个objc_class的结构体,指向的本类的父类的objc_class
结构体。cache
用来处理已经调用方法的缓存。 class_data_bits_t
是objc_class 的关键,很多变量都是根据 bits
来实现的。
对象的初始化
在对象初始化的时候,一般都会调用 alloc+init 方法进行实例化,或者通过new 方法。
- 第一步:调用系统的alloc 方法 或者new 方法(其中`new`方法直接调用的`callAlloc init`)
+ (instancetype)alloc OBJC_SWIFT_UNAVAILABLE("use object initializers instead");
+(id)alloc{
return _objc_rootAlloc(self);
}
- 第二步: runtime 内部实现调用objc_rootAlloc 方法
// Base class implementation of +alloc. cls is not nil.
// Calls [cls allocWithZone:nil].
id
_objc_rootAlloc(Class cls)
{
return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}
- 第三步: callAlloc 方法实现,解析:
callAlloc 方法在创建对象的地方有两种方式,一种是通过calloc
开辟内存,然后通过obj->initInstanceIsa(cls, dtor)
函数初始化这块内存。 第二种是直接调class_createInstance
函数,由内部实现初始化逻辑 ;
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false) {
if (fastpath(cls->canAllocFast())) {
bool dtor = cls->hasCxxDtor();
id obj = (id)calloc(1, cls->bits.fastInstanceSize());
if (slowpath(!obj)) return callBadAllocHandler(cls);
obj->initInstanceIsa(cls, dtor);
return obj;
} else {
id obj = class_createInstance(cls, 0);
if (slowpath(!obj)) return callBadAllocHandler(cls); return obj;
}
}
但是在最新的objc-723
中,调用canAllocFast()
函数直接返回false ,所以只会执行上面所述的第二个else
代码块。
bool canAllocFast(){
return false;
}
初始化的代码最终会调用到 _class_createInstanceFromZone
函数,这个函数是初始化的关键代码。然后通过instanceSize 函数返回的 size
,并通过calloc
函数分配内存,初始化isa_t
指针。
size_t size = cls->instanceSize(extraBytes);
obj->initIsa(cls);
消息的发送机制
在OC 中方法调用时通过Runtime 来实现的,runtime 进行方法调用本质上是发送消息,通过objc_msgSend()
函数来进行消息的发送
[MyClass classMethod]
在runtime运行时被转换为 ((void ()(id, SEL))(void )objc_msgSend)((id)objc_getClass("MyClass"), sel_registerName("classMethod"));
相关的demo可见我个人的github,在目录文件中,我将main.m进行了OC->C的转换:RuntimeDemo
上述的方法可以理解为 向一个objc_class发送了一个SEL 。
OC中每一个Method
的结构体如下:
struct objc_method {
SEL _Nonnull method_name
char * _Nullable method_types
IMP _Nonnull method_imp
}
在新的objc_runtime_new.h
中objc_method
已经没有使用了,使用的是如下的结构体,其引入的方式也发生了改变,不是直接定义在objc_class
类中,而是通过getLoadMethod
方法来实现间接的调用。
struct method_t {
SEL name;
const char *types;
IMP imp;
struct SortBySELAddress :
public std::binary_function<const method_t&,
const method_t&, bool>
{
bool operator() (const method_t& lhs,
const method_t& rhs)
{ return lhs.name < rhs.name; }
};
};
objc_msgSend
就是通过SEL
来进行遍历查找的,如果两个类定义了相同名称的方法,它们的SEL
就是一样的。
objc_method
中具体参数解析如下:
SEL
指的就是第一步中解析方法调用得到的sel_registerName(“methodName”)
的返回值。method_types
指的是返回值的类型和参数。以返回值为开始,依次把参数拼接在后面,类型对应表格链接[TYPE EDCODING]。(联想一哈,这个东西也是类似于property_gerAttrubute一样,有对应的类型关系,某个字符意味着某种类型)IMP_Method
参数 是一个函数指针,指向objc_method所对应的实现部分。
objc_msgSend 工作原理
当一个对象被创建,系统会为通过上述的callalloc
函数分配一个内存size
并给他初始化一个isa
指针,可以通过指针访问其类对象,并且通过对类对象访问其所有继承者链中的类。
-
objc_msgSend 底层实现没有完全的暴露出来,但是通过源码中的
objc-msg-simulator-x86_64.s
的第672行代码开始可以看到部分实现,也可以通过Xcode
断点来查看运行的堆栈信息。其实现原理主要是通过2个方法来完成,首先是CacheLookup
方法,在缓存中没有存在的情况下会去执行__objc_msgSend_uncached
的MethodTable
查找SEL
GetIsaCheckNil NORMAL // r10 = self->isa, or return zero CacheLookup NORMAL, CALL // calls IMP on success GetIsaSupport NORMAL NilTestReturnZero NORMAL // cache miss: go search the method lists LCacheMiss: // isa still in r10 MESSENGER_END_SLOW jmp __objc_msgSend_uncached END_ENTRY _objc_msgSend
__objc_msgSend_uncached
方法查找STATIC_ENTRY __objc_msgSend_uncached UNWIND __objc_msgSend_uncached, FrameWithNoSaves // THIS IS NOT A CALLABLE C FUNCTION // Out-of-band x16 is the class to search MethodTableLookup br x17 END_ENTRY __objc_msgSend_uncached STATIC_ENTRY __objc_msgLookup_uncached UNWIND __objc_msgLookup_uncached, FrameWithNoSaves // THIS IS NOT A CALLABLE C FUNCTION // Out-of-band x16 is the class to search MethodTableLookup ret
-
在执行
MethodTableLookup
方法时其中调用到了__class_lookupMethodAndLoadCache3
去找到需要的Class
参数和SEL
,内部实现找IMP
的是操作 方法是lookUpImpOrForward
。 -
当对象接受到消息时,runtime会沿着消息函数的
isa
查找对应的类对象,然后是先在objc_cache
中去查找当前的SEL
的缓存,如果缓存中存在SEL
,就直接返回该IMP
也就是该实现方法的指针。 -
如果cache 中不存在缓存,需要先判断该类是否已经被创建,如果没有,则将类实例化,第一次调用当前类的话,执行
initialized
代码,再开始读取这个类的缓存,还是没有的情况下才在method list 中查找方法selector。本类如果没有,就会到父类的method list中去查找缓存和method list 中的SEL
,直到NSObject
类 。
//如果缓存在就直接返回
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
runtimeLock.read();
// 看看类有没有被初始化,没有初始化就直接初始化
if (!cls->isRealized()) {
// Drop the read-lock and acquire the write-lock.
// realizeClass() checks isRealized() again to prevent
// a race while the lock is down.
runtimeLock.unlockRead();
runtimeLock.write();
realizeClass(cls);
runtimeLock.unlockWrite();
runtimeLock.read();
}
//走一遍 initialized 方法
if (initialize && !cls->isInitialized()) {
runtimeLock.unlockRead();
_class_initialize (_class_getNonMetaClass(cls, inst));
runtimeLock.read();
// If sel == initialize, _class_initialize will send +initialize and
// then the messenger will send +initialize again after this
// procedure finishes. Of course, if this is not being called
// from the messenger then it won't happen. 2778172
}
retry:
runtimeLock.assertReading();
4.如果在类的继承体系中都没有找到SEL
,则会进行动态消息解析,给自己保留处理找不到方法的机会,
// 没有找到该方法,会执行下面的分解方法
if (resolver && !triedResolver) {
runtimeLock.unlockRead();
_class_resolveMethod(cls, sel, inst);
runtimeLock.read();
// Don't cache the result; we don't hold the lock so it may have
// changed already. Re-do the search from scratch instead.
triedResolver = YES;
goto retry;
}
其中_class_resolveMethod
的源码解析为:
if(!cls->isMetaClass()){
_class_resolveInstanceMethod(cls, sel, inst);
}else{
_class_resolveClassMethod(cls, sel, inst);
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
_class_resolveInstanceMethod(cls, sel, inst);
}
}
- 动态消息解析如果没有做出响应,则进入动态消息转发阶段,如果还没有人响应,就会触发
doesNotRecognizeSelector
此时可以在动态消息转发阶段做一些处理,否则就会Crash
.
消息转发机制
消息转发机制的实现:
首先从上述的_class_resolveMethod
可以方法可以看到,在找不到相关实现方法的时候,最终执行的都是_class_resolveInstanceMethod
方法,那我们就从这个方法来进行剖析。
_class_resolveMethod
方法实现所属类动态方法的解析,其中主要的函数_class_resolveInstanceMethod
方法本质还是给指定类发送一个objc_msgSend
消息。经过各层级查找后还是没有,就会返回nil。但是iOS提供了用户处理返回nil 后会出现闪退的方案,也就是resolveInstanceMethod
方法,从 option 键查看描述可以得到其内部实现的是addMethod
方法。- 在对象所属类不能动态添加方法后,
runtime
又提供了其他对象可以处理这个未知的SEL
的方法,相关方法声明如下:
- (id)forwardingTargetForSelector:(SEL)aSelector;
- 在上述2种方法都没有被实现的情况下,就只剩下最后一次机会,那就是消息重定向。这个时间
runtime
会把SEL
封装成NSInvocation
对象,然后调用:
- (void)forwardInvocation: (NSInvocation*)invocation;
如果这个类不能处理,就会调用其父类,知道NSObject
也没有找到这个方法就会报错doesNotRecognizeSelector
抛出异常,并且闪退.
上述的消息动态转发是要人为的去实现,如果没实现在动态转发,在执行到动态解析之后就会发生闪退。
实战使用
Runtime 为类别动态添加属性
思考:
- 类的属性是怎么实现的?
- 在类别中添加属性为什么会不成功?
- 使用动态时实现在类别中新增属性的原理是什么?
类的属性实现原理
- 在类中使用@property,系统会自动生成带__ 的成员变量,和该变量的Setter 和getter 方法。 也就是意味着 一个成员属性(property) 就相当于 成员变量+setter+getter 方法
unsigned int invarCount = 0;
Ivar *invars = class_copyIvarList([Human class], &invarCount);
for (NSInteger i =0; i<invarCount; i++) {
Ivar ivar = invars[i];
NSLog(@"获取到的成员变量%s",ivar_getName(ivar));
}
unsigned int outCount = 0;
Method *method = class_copyMethodList([Human class], &outCount);
for (NSInteger i= 0; i< outCount; i++) {
NSLog(@"method%s", sel_getName(method_getName(method[i]))); // 4
}
objc_property_t *propertys = class_copyPropertyList([Human class], &outCount);
for (unsigned int i = 0; i<outCount; i++) {
objc_property_t property = propertys[i];
NSString *propertyName = [[NSString alloc]initWithCString:property_getName(property) encoding:NSUTF8StringEncoding];
NSLog(@"propertyName%@",propertyName);
}
解析:使用property 会自动生成成员变量和getter、setter 函数
类别中直接添加属性剖析
- 在类的类别中添加属性,系统不会生成该属性的 成员变量+Setter+getter 方法
@interface UIImage (SubImage)
@property(nonatomic,strong)NSString *imageString;
@end
输出log 为: 获取到的成员变量_imageRef
获取到的成员变量_scale
获取到的成员变量_imageFlags
获取到的成员变量_flipsForRightToLeftLayoutDirection
获取到的成员变量_traitCollection
获取到的成员变量_vectorImageSupport
获取到的成员变量_imageAsset
获取到的成员变量_alignmentRectInsets
解析:category 它是在运行期决议的。 因为在运行期即编译完成后,对象的内存布局已经确定。
使用runtime 为类别添加属性
思考:
1.runtime 为什么能给类别添加属性?
2.runtime 实现给类别添加属性的原理是什么?
在OC 中,类别即category 也是一个结构体categroy_t
,具体的定义如下:
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;
// Fields below this point are not always present on disk.
struct property_list_t *_classProperties;
method_list_t *methodsForMeta(bool isMeta) {
if (isMeta) return classMethods;
else return instanceMethods;
}
property_list_t *propertiesForMeta(bool isMeta, struct header_info *hi);
};
从上述的代码我们不难看出category 是带有协议、实例方法、类方法的参数的,而在runtime 进行初始化即调用objc_init
的时候,最后会有调用_dyld_objc_notify_register(&map_images, load_images, unmap_image)
。在其内部有一个_read_images
的操作会去取出当前类对应的category 数组,并将其中的每个category_t
对象取出,最终执行addUnattachedCategoryForClass
函数添加到category 哈希表中。然后通过remethodizeClass
方法来添加到指定的Class
上。
具体源码见:
objc-runtime-new.mm
- 在给类别添加属性的时候需要通过runtime 来为该属性手动实现getter 和setter 方法
//实现代码的getter 方法
-(NSTimeInterval)timeInterval{
return [objc_getAssociatedObject(self, _cmd)doubleValue];
}
实现代码的setter 方法
-(void)setTimeInterval:(NSTimeInterval)timeInterval{
objc_setAssociatedObject(self, @selector(timeInterval), @(timeInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
解析:通过runtime 可以对类的属性动态绑定 生成getter 和setter 方法,从category的结构体中可以看到,但是却无法生存实例变量。
参考资料:
Runtime 实现方法交换
Method Swizzle 实现的原理
在现实开发中,我们会遇到一些需求,比如为防止按钮重复点击、检测所有界面执行viewdidload
、之类的操作时,Method 可以起到很不错的效果。这中编程方式也是属于 面向切向编程(AOP
)的一种实现方式。在iOS
中一个典型的第三方框架 Aspects。
-
在上面曾讲到过
objc_method
中SEL
就代表着方法名称,IMP
代表着对应的实现方法。所以我们可以讲 Methodswizzle 做的就是将两个方法的SEL
和IMP
进行对应的交换。
如图所示,对应的SEL 是指向对应的IMP 的,方法交换要实现的就是把SEL 指向的IMP 方法进行交换。
-
在实现把对应的方法进行交换时,我们通常会在一个类的类别中来实现,在
load
方法中执行所要交换的两个方法。 因为load
方法在程序运行时就被调用加载到内存中了,有关load
和initialize
之间的差别其中有一点就是调用时机,load 在这个类中只会被调用一次 ,而initialize 在第一次发送消息的时候才会调用。所以在load
中来实现方法交换会更加的合适。
Methoad swizzle 实现代码
- 首先要获取到当前用于交换的方法 。runtime 提供了2种形式来获取:
1.获取Method ,交换的方法为实例方法
class_getInstanceMethod([self Class],SEL oldSel);
2.获取Method ,交换的方法为类方法
class_getClassMethod([self Class],SEL oldSel);
- 对需要进行交换的方法进行验证,保证该类实现了这个方法,而不是他的父类实现了,这样就达不到想要的交换本类方法的效果。
class_addMethod
在runtime 内部实现注释为 :
添加一个新的方法到指定类,并为其指定方法名和实现方法(即SEL
和IMP
)。在添加成功时,返回YES
,否则NO
,该方法将添加超类的实现覆盖,但是不会替换此类中的已经存在的实现方法,要更改现有实现方法,请使用method_setImplementation
.
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types)
在返回失败的时候,说明该方法本身就已经存在,只要执行交换操作就可以了,否则就执行replace 操作。
- 实现方法之间的交换
method_exchangeImplementations(oldMethod, newMethod);
注意:在使用方法交换的时候要记得使用 单例,为了避免出现第一次交换之后,第二次又给换回来的情况。