Runtime源码解析和实战使用

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.hobjc_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 指针,可以通过指针访问其类对象,并且通过对类对象访问其所有继承者链中的类。

  1. objc_msgSend 底层实现没有完全的暴露出来,但是通过源码中的objc-msg-simulator-x86_64.s的第672行代码开始可以看到部分实现,也可以通过Xcode断点来查看运行的堆栈信息。其实现原理主要是通过2个方法来完成,首先是CacheLookup方法,在缓存中没有存在的情况下会去执行 __objc_msgSend_uncachedMethodTable查找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
    
  2. 在执行MethodTableLookup方法时其中调用到了__class_lookupMethodAndLoadCache3 去找到需要的Class参数和SEL,内部实现找IMP 的是操作 方法是lookUpImpOrForward

  3. 当对象接受到消息时,runtime会沿着消息函数的isa查找对应的类对象,然后是先在objc_cache中去查找当前的SEL 的缓存,如果缓存中存在SEL,就直接返回该IMP也就是该实现方法的指针。

  4. 如果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);
        }
}
  1. 动态消息解析如果没有做出响应,则进入动态消息转发阶段,如果还没有人响应,就会触发doesNotRecognizeSelector 此时可以在动态消息转发阶段做一些处理,否则就会Crash.

消息转发机制

消息转发机制的实现:
首先从上述的_class_resolveMethod可以方法可以看到,在找不到相关实现方法的时候,最终执行的都是_class_resolveInstanceMethod方法,那我们就从这个方法来进行剖析。

  1. _class_resolveMethod方法实现所属类动态方法的解析,其中主要的函数_class_resolveInstanceMethod 方法本质还是给指定类发送一个objc_msgSend消息。经过各层级查找后还是没有,就会返回nil。但是iOS提供了用户处理返回nil 后会出现闪退的方案,也就是resolveInstanceMethod方法,从 option 键查看描述可以得到其内部实现的是 addMethod方法。
  2. 在对象所属类不能动态添加方法后,runtime又提供了其他对象可以处理这个未知的SEL的方法,相关方法声明如下:
    - (id)forwardingTargetForSelector:(SEL)aSelector;
  3. 在上述2种方法都没有被实现的情况下,就只剩下最后一次机会,那就是消息重定向。这个时间runtime会把SEL封装成NSInvocation对象,然后调用:
    - (void)forwardInvocation: (NSInvocation*)invocation;
    如果这个类不能处理,就会调用其父类,知道NSObject也没有找到这个方法就会报错doesNotRecognizeSelector 抛出异常,并且闪退.

上述的消息动态转发是要人为的去实现,如果没实现在动态转发,在执行到动态解析之后就会发生闪退。

实战使用

Runtime 为类别动态添加属性

思考:

  1. 类的属性是怎么实现的?
  2. 在类别中添加属性为什么会不成功?
  3. 使用动态时实现在类别中新增属性的原理是什么?
类的属性实现原理
  • 在类中使用@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的结构体中可以看到,但是却无法生存实例变量。

参考资料:

美团技术团队-category的深入理解

Runtime 实现方法交换

Method Swizzle 实现的原理

在现实开发中,我们会遇到一些需求,比如为防止按钮重复点击、检测所有界面执行viewdidload 、之类的操作时,Method 可以起到很不错的效果。这中编程方式也是属于 面向切向编程(AOP)的一种实现方式。在iOS 中一个典型的第三方框架 Aspects

  • 在上面曾讲到过objc_methodSEL 就代表着方法名称,IMP 代表着对应的实现方法。所以我们可以讲 Methodswizzle 做的就是将两个方法的SELIMP进行对应的交换。
    cd1c1c58f7c219d8829117f7e954cacd.png
    如图所示,对应的SEL 是指向对应的IMP 的,方法交换要实现的就是把SEL 指向的IMP 方法进行交换。
    c3e7c1b6ce66f851a86c3f0e3a8702c3.png

  • 在实现把对应的方法进行交换时,我们通常会在一个类的类别中来实现,在load方法中执行所要交换的两个方法。 因为load方法在程序运行时就被调用加载到内存中了,有关loadinitialize 之间的差别其中有一点就是调用时机,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 内部实现注释为 :
    添加一个新的方法到指定类,并为其指定方法名和实现方法(即SELIMP)。在添加成功时,返回YES,否则NO,该方法将添加超类的实现覆盖,但是不会替换此类中的已经存在的实现方法,要更改现有实现方法,请使用method_setImplementation.
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types) 

在返回失败的时候,说明该方法本身就已经存在,只要执行交换操作就可以了,否则就执行replace 操作。

  • 实现方法之间的交换
method_exchangeImplementations(oldMethod, newMethod);

注意:在使用方法交换的时候要记得使用 单例,为了避免出现第一次交换之后,第二次又给换回来的情况。

runtime 源码下载

猜你喜欢

转载自blog.csdn.net/Abe_liu/article/details/85704188