zl程序教程

您现在的位置是:首页 >  移动开发

当前栏目

IOS底层原理 -5.运行时(1)

ios原理 运行 底层
2023-09-27 14:26:28 时间

OC是一种动态性比较强的语言,所有的函数调用都是基于消息机制;简介参照:

1. isa指针

1.1 简述

 ***注意:以下的分析都是基于arm64***

isa在前面介绍过,实例对象可以通过 isa找到类对象,类对象通过isa可以找到原类对象;在arm64后isa并不直接是Class类型,而是union,同时用位域位域(w3c)来存储更多信息(struct test{uintptr_r nonpointer :1; }),

1.2 在看isa之前先熟悉两个知识点位域共用体union

  char _bool;//将所有BOOL值存储到一个字节中
  -(void)setTall:(BOOL)tall{
   	if(tall){
      	 _bool |=  (1<<1)}else{
    	 _bool &=  ~(1<<1)}
  }
   -(void)setRich:(BOOL)rich{
      if(rich){
      	 _bool |=  (1<<0)}else{
    	 _bool &=  ~(1<<0)}
      
  }
  - (BOOL)isRich{
     return !!(_bool&(1<<0));//最高位存储rich
  }
 - (BOOL)isTall{
   	return !!(_bool&(1<<1));//第二位存储Tall
  }

接下来看下位域中怎么实现:

  @interface LYMPerson()
{
    // 位域
    struct {
        char tall : 1;
        char rich : 1;
    } _bool;
}
@end

@implementation LYMPerson

- (void)setTall:(BOOL)tall
{
    _bool.tall = tall;
}

- (BOOL)isTall
{
    return !!_bool.tall;
}

- (void)setRich:(BOOL)rich
{
    _bool.rich = rich;
}

- (BOOL)isRich
{
    return !!_bool.rich;
}
@end
//简化的源码
	union isa_t //共用体
	{
	    isa_t() { }
	    isa_t(uintptr_t value) : bits(value) { }
	
	    Class cls;
	    uintptr_t bits;//存放所有的数据
	
	struct {//位域
	        uintptr_t nonpointer        : 1;
	        uintptr_t has_assoc         : 1;
	        uintptr_t has_cxx_dtor      : 1;
	        uintptr_t shiftcls          : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
	        uintptr_t magic             : 6;
	        uintptr_t weakly_referenced : 1;
	        uintptr_t deallocating      : 1;
	        uintptr_t has_sidetable_rc  : 1;
	        uintptr_t extra_rc          : 19;
	    };
	
	};

1.3 isa结构体的成员的含义:

  • 结构体基本成员介绍
nonpointerhas_assoc
0 代表普通指针,存储着Class、Meta-Class对象的内存地址;1 代表优化过,使用位域存储更多的信息是否设置过关联对象,如果没有,释放时更快
has_cxx_dtorshiftcls
是否有c++的析构函数(cxx_destruct),如果没有,释放时会更快存储着Class,Meta-Class对象的内存地址信息
magicweakly_referenced
用于在调试时分辨对象是否未完成初始化是否有被弱引用指向如果没有,释放速度会更快
deallocatinghas_sidetable_rc
对象是否正在释放里面存储的值的引用计数器减1
extra_rc
引用计数器是否过大无法存储在isa中,如果为1那么引用计数会存储在一个叫SideTable的类属性中
  • 下面来验证一下
    在这里插入图片描述
    加上weak和关联属性后的值
#import "ViewController.h"

#import <objc/message.h>
#import <objc/runtime.h>

@interface LYMAnimal:NSObject
@property(nonatomic,assign,readwrite) NSInteger age;
@end
@implementation LYMAnimal

@end
@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    LYMAnimal *animal = [[LYMAnimal alloc]init];
    __weak LYMAnimal *weakSelf = animal;
    
    objc_setAssociatedObject(animal, @"name", @"haha", OBJC_ASSOCIATION_COPY);
    id va = objc_getAssociatedObject(animal, @"name");
    NSLog(@"%@ %@",animal,va);
}


@end

在这里插入图片描述
通过对比两张图可以看出,加了weak和关联对象后相应的位的值都有改变,weakly_refrenced的变成了1,has_assoc对应的位变成了1,这与表格的描述刚好符合;

1.4 isa扩展

  • 在之前实例对象/类对象通过isa找到类对象/原类对象前都必须的&ISA_MASK;而在源码中的定义为ISA_MASK 0x0000000ffffffff8ULL,在联合体中shiftcls存储真实地址只占33位,因此&ISA_MASK刚好可以取出类对象或者原类对象的真实地址;同时也可以知道类对象或者原类对象的最后三位为零,通过一段代码验证一下:

在这里插入图片描述

  • 从上图的输出可知最后一位全是0和8,而16进制的0和8 对应的二进制是 0000和1000,那么可以验证实例对象和类对象、原类对象的内存地址最后三位都是0

Class

  1. 照例看下图:
    在这里插入图片描述
  2. 下面详细介绍下这个结构体的成员:

class_rw_t:可读可写,里面的methods和propeties及protocols都是二位数组,而ro指向的结构体class_ro_t(只读)里面的baseMethodList…是一维的,其中baseMethodList中是method_t类型;
method_t:结构体包含 IMP imp,指向函数地址的指针;SEL name 函数名;const chat *type;返回值类型,参数类型;

  • IMP代表函数的具体实现,typedef id _Nullable (*IMP)(id __Nonnull,SEL _nonnull ,...);

  • SEL代表方法/函数名,也叫做选择器,底层结构和char类似,可以通过@selector()、sel_registerName()获得,不同类中相同名字的方法,所对应的方法选择器是相同的,它的底层是:
    typedef struct objc_selector *SEL

  • types 包含了函数的返回值,参数的编码信息;v@:v代表返回值是void,@代表参数id类型,代表参数sel,@encode()指令,可以将具体的类型表示成字符串编码,苹果在其官方文档中也有说明;这里列举部分如下图:

在这里插入图片描述
3. 方法缓存(cache_t

  • 在之前研究objc_class结构体的时候,我们知道在Class结构体内部有一个方法缓存结构体(cache_t cache),使用散列表来缓存使用过的方法来提高方法的查找速度;

在这里插入图片描述

  • 散列表:是一个长度不固定的列表,当长度不够存储的时候会重新创建后将原来的释放掉,主要是通过一定的算法将数据存入内存列表的指定索引位置,查找的时候根据同一算法算出的索引直接去列表中取出,当只有一个数据需要存储的时候,算出的索引在什么位置就存储在什么索引位置,该索引以外的其他索引存储为NULL;因此可以说散列表牺牲了部分内存换取查找速度;oc中如果算出的索引相同继续比较key,如果key不相同则将值减1,依次查找直到找到;
  • 顺序:实例对象isa–> 类对象–>类对象的cache中查找,没有–>methodList中找,找到返回,同时加入cache中
    如果methodList找没有找到–>superclass找到父类–>cache中查找–>找到返回,同时缓存到类对象的cache中
    如果在父类的cache找没有找到–>methodList中查找–>找到返回,同时缓存到类对象的cache中; 即:如下图
    在这里插入图片描述

2. objc_msgSend(id,SEL);OC中的方法调用

2.1 简述

OC中的方法调用都是基于消息发送即:objc_msgSend(id,SEL);,看一个简单的示例:

 LYMAnimal *animal = [[LYMAnimal alloc]init];
  [animal callEat];

在执行xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc -fobjc-arc -fobjc-runtime=ios-12.0.0 main.m的c++文件里最后里(约32000多行)如下:

#pragma clang assume_nonnull end
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; 

        NSLog((NSString *)&__NSConstantStringImpl__var_folders_v7_xv8f6lp50hbcxftgjf7yb_sw0000gn_T_main_773e9c_mi_0);
        LYMAnimal *animal = ((LYMAnimal *(*)(id, SEL))(void *)objc_msgSend)((id)((LYMAnimal *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LYMAnimal"), sel_registerName("alloc")), sel_registerName("init"));
        ((void (*)(id, SEL))(void *)objc_msgSend)((id)animal, sel_registerName("callEat"));
    }
    return 0;
}

名词: receiver
从上述代码可以看到在main函数里[animal callEat];最终转换成了objc_msgSend;objc_msgSend在苹果开源的运行时代码里是以汇编实现的,因为这部分代码肯定是调用频率非常的高;

2.2 执行阶段:消息发送

ENTRY _objc_msgSend
	UNWIND _objc_msgSend, NoFrame
	MESSENGER_START
//x0寄存器:消息接收者,判断消息接受者是不是(<0)空,是则跳转到LNilOrTagged 然后LReturnZero最后返回(ret)
	cmp	x0, #0			// nil check and tagged pointer check
	b.le	LNilOrTagged		//  (MSB tagged pointer looks negative) //进行判断如果上述成立就跳转
	ldr	x13, [x0]		// x13 = isa
	and	x16, x13, #ISA_MASK	// x16 = class	
LGetIsaDone:
	CacheLookup NORMAL		// calls imp or objc_msgSend_uncached   //这里开始查找缓存

LNilOrTagged:
	b.eq	LReturnZero		// nil check

	// tagged
	mov	x10, #0xf000000000000000
	cmp	x0, x10
	b.hs	LExtTag
	adrp	x10, _objc_debug_taggedpointer_classes@PAGE
	add	x10, x10, _objc_debug_taggedpointer_classes@PAGEOFF
	ubfx	x11, x0, #60, #4
	ldr	x16, [x10, x11, LSL #3]
	b	LGetIsaDone

LExtTag:
	// ext tagged
	adrp	x10, _objc_debug_taggedpointer_ext_classes@PAGE
	add	x10, x10, _objc_debug_taggedpointer_ext_classes@PAGEOFF
	ubfx	x11, x0, #52, #8
	ldr	x16, [x10, x11, LSL #3]
	b	LGetIsaDone
	
LReturnZero:
	// x0 is already zero
	mov	x1, #0
	movi	d0, #0
	movi	d1, #0
	movi	d2, #0
	movi	d3, #0
	MESSENGER_END_NIL
	ret  //相当于return

	END_ENTRY _objc_msgSend

首先会判断消息接受者(receiver)是不是空的,空的直接返回;如果不是空则在缓存中查找CacheLookup

.macro CacheLookup
	// x1 = SEL, x16 = isa
	ldp	x10, x11, [x16, #CACHE]	// x10 = buckets, x11 = occupied|mask
	and	w12, w1, w11		// x12 = _cmd & mask
	add	x12, x10, x12, LSL #4	// x12 = buckets + ((_cmd & mask)<<4)

	ldp	x9, x17, [x12]		// {x9, x17} = *bucket
1:	cmp	x9, x1			// if (bucket->sel != _cmd)
	b.ne	2f			//     scan more
	CacheHit $0			// call or return imp
	
2:	// not hit: x12 = not-hit bucket
	CheckMiss $0			// miss if bucket->sel == 0
	cmp	x12, x10		// wrap if bucket == buckets
	b.eq	3f
	ldp	x9, x17, [x12, #-16]!	// {x9, x17} = *--bucket
	b	1b			// loop

3:	// wrap: x12 = first bucket, w11 = mask
	add	x12, x12, w11, UXTW #4	// x12 = buckets+(mask<<4)

	// Clone scanning loop to miss instead of hang when cache is corrupt.
	// The slow path may detect any corruption and halt later.

	ldp	x9, x17, [x12]		// {x9, x17} = *bucket
1:	cmp	x9, x1			// if (bucket->sel != _cmd)
	b.ne	2f			//     scan more
	CacheHit $0			// call or return imp 查找到返回IMP
	
2:	// not hit: x12 = not-hit bucket
	CheckMiss $0			// miss if bucket->sel == 0
	cmp	x12, x10		// wrap if bucket == buckets
	b.eq	3f
	ldp	x9, x17, [x12, #-16]!	// {x9, x17} = *--bucket
	b	1b			// loop

3:	// double wrap
	JumpMiss $0//缓存中没有找到跳转
	
.endmacro

在上述汇编中进行查找,没有找到跳转到JumpMiss

.macro JumpMiss
.if $0 == GETIMP
	b	LGetImpMiss
.elseif $0 == NORMAL
	b	__objc_msgSend_uncached//这个是一个C语言的函数__class_lookupMethodAndLoadCache3
.elseif $0 == LOOKUP
	b	__objc_msgLookup_uncached
.else
.abort oops
.endif
.endmacro

接着便是在这个函数中执行:

IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)//汇编代码比c代码多一个"_"
{
    return lookUpImpOrForward(cls, sel, obj, 
                              YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}

lookUpImpOrForward中会重新查找一次缓存,因为这里有可能动态添加方法;这个方法也是最核心的方法:

IMP lookUpImpOrForward(Class cls, SEL sel, id inst, 
                       bool initialize, bool cache, bool resolver)
{
    IMP imp = nil;
    bool triedResolver = NO;

    runtimeLock.assertUnlocked();

    // Optimistic cache lookup
    if (cache) {
        imp = cache_getImp(cls, sel);
        if (imp) return imp;
    }

    // runtimeLock is held during isRealized and isInitialized checking
    // to prevent races against concurrent realization.

    // runtimeLock is held during method search to make
    // method-lookup + cache-fill atomic with respect to method addition.
    // Otherwise, a category could be added but ignored indefinitely because
    // the cache was re-filled with the old value after the cache flush on
    // behalf of the category.

    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();
    }

    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();

    // Try this class's cache.

    imp = cache_getImp(cls, sel);//会再次去缓存中查找
    if (imp) goto done;

    // Try this class's method lists. 去方法列表中查找
    {
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            imp = meth->imp;
            goto done;
        }
    }

    // Try superclass caches and method lists. 去父类的缓存和方法列表中查找
    {
        unsigned attempts = unreasonableClassCount();
        for (Class curClass = cls->superclass;
             curClass != nil;
             curClass = curClass->superclass)
        {
            // Halt if there is a cycle in the superclass chain.
            if (--attempts == 0) {
                _objc_fatal("Memory corruption in class list.");
            }
            
            // Superclass cache.
            imp = cache_getImp(curClass, sel);
            if (imp) {
                if (imp != (IMP)_objc_msgForward_impcache) {
                    // Found the method in a superclass. Cache it in this class.
                    log_and_fill_cache(cls, imp, sel, inst, curClass);//父类中找到后缓存到当前类中
                    goto done;
                }
                else {
                    // Found a forward:: entry in a superclass.
                    // Stop searching, but don't cache yet; call method 
                    // resolver for this class first.
                    break;
                }
            }
            
            // Superclass method list.
            Method meth = getMethodNoSuper_nolock(curClass, sel);
            if (meth) {
                log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
                imp = meth->imp;
                goto done;
            }
        }
    }

    // No implementation found. Try method resolver once.

    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;
    }

    // No implementation found, and method resolver didn't help. 
    // Use forwarding.

    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlockRead();

    return imp;
}

通过上述代码可知:方法调用时候,底层转换成消息发送(obj_msgSend),首先如果有缓存则在缓存中查找,缓存中没有则在方法列表中查找,在方法列表中进行查找的时候会执行一次缓存查找避免该过程中动态加入的方法导致的问题;如果方法列表找那个也没有则去父类的缓存和方法列表中查找;

2.3 执行阶段:动态方法解析 (dynamic method resolution)

在其父类中还找不到方法且父类没有父类,那么就会进入动态方法解析,这里会调用+(BOOL)resolveInstanceMethod:(SEL)sel,这里可以动态添一次方法,然后会重新开始一次方法查找:

2.3.1 实例方法动态添加

在动态添加之前会判断是不是已经动态添加过,如果添加过就不会添加;就是直接到消息转发阶段
void callEat(id self,SEL _cmd){
    NSLog(@"callEat 动态添加");
}
@implementation LYMAnimal
//-(void)callEat{
//    NSLog(@"eat======");
//}
struct method_t {
    SEL sel;
    char *types;
    IMP imp;
};
//-(void)otherCall{
//    NSLog(@"%s",__func__);
//}
//+(BOOL)resolveInstanceMethod:(SEL)sel{
//    if (sel == @selector(callEat)) {
//        struct method_t *meth = (struct method_t *)class_getInstanceMethod(self, @selector(otherCall));
//        //动态添加方法
//        class_addMethod(self, sel, meth->imp, meth->types);
//        return YES;
//    }
//    return [super resolveInstanceMethod:sel];
//}
+(BOOL)resolveInstanceMethod:(SEL)sel{//对象方法
    if (sel == @selector(callEat)) {
        //动态添加方法
        class_addMethod(self, sel, (IMP)callEat, "v16@0:8");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
@end

2.3.2 类方法动态添加:

+(BOOL)resolveClassMethod:(SEL)sel{
    if (sel == @selector(callEat1)) {
        //动态添加方法
        class_addMethod(object_getClass(self), sel, (IMP)callEat, "v16@0:8");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

如果这一步没有处理,那么就会到消息转发;

2.4 执行阶段:消息转发

消息转发有三个阶段,各个阶段关系如图(图来自:<<Effective-Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法>>):
在这里插入图片描述

-(id)forwardingTargetForSelector:(SEL)aSelector

先看一个经典错误:
在这里插入图片描述

消息转发部分源码是没有开源,但是从上图可以知道:

  • 先调用__forwarding__ 然后在该方法里会调用forwardingTargetForSelector,该方法里可以返回能处理该消息的对象
  • 如果上面的方法返回nil,则会调用methodSignatureForSelector
    补充一个知识点: NSInvocation 封装了一个方法调用,包括方法调用者,方法名,方法参数,更改其target属性可以修改其方法调用者;

-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    if (aSelector == @selector(callEat)) {
        return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];//这里不是空的就会调用forwardInvocation
    }
    return [super methodSignatureForSelector:aSelector];
}
-(void)forwardInvocation:(NSInvocation *)anInvocation{
    
}

注意:类对象消息转发对应的方法:

//+(id)forwardingTargetForSelector:(SEL)aSelector
+(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    if (aSelector == @selector(callEat1)) {
        return [NSMethodSignature signatureWithObjCTypes:"v16@0:8"];
    }
    return [super methodSignatureForSelector:aSelector];
}
//NSInvocation 封装了一个方法调用,包括方法调用者,方法名,参数,返回值
+(void)forwardInvocation:(NSInvocation *)anInvocation{
    
}

对应输出:
在这里插入图片描述
在这里插入图片描述

2.4 最后

应用来自《Effective-Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法》的一段总结:

接收者在每一步中均有机会处理消息。步骤越往后,处理消息的代价就越大。最好能在第一步就处理完,这样的话,运行期系统就可以将此方法缓存起来了。如果这个类的实例稍后还收到同名选择子,那么根本无须启动消息转发流程。若想在第三步里把消息转给备援的接收者,那还不如把转发操作提前到第二步。因为第三步只是修改了调用目标,这项改动放在第二步执行会更为简单,不然的话,还得创建并处理完整的NSInvocation。

3. super关键字

3.1 一个网上经典的面试题:

        NSLog(@"self class = %@",[self class]);
        NSLog(@"self superclass = %@",[self superclass]);
        
        NSLog(@"super class = %@",[super class]);
        NSLog(@"super superclass =%@",[super superclass]);

输出结果是:
在这里插入图片描述
super是一个编译器标示,告诉方法调用时候,从父类的方法中去找方法的实现,但是消息的接收者还是自己(子类对象);
其详细的底层实现解释请参考:iOS-底层原理17

3.2 底层分析

先看一下转换成c++文件中对应的结构:

struct __rw_objc_super { 
	struct objc_object *object; 
	struct objc_object *superClass; 
	__rw_objc_super(struct objc_object *o, struct objc_object *s) : object(o), superClass(s) {} 
};
// @implementation LYMWorker

static void _I_LYMWorker_eat(LYMWorker * self, SEL _cmd) {
    ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("LYMWorker"))}, sel_registerName("eat"));
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_v7_xv8f6lp50hbcxftgjf7yb_sw0000gn_T_LYMWorker_b54bca_mi_0,__func__);
}
// @end

简化一下函数里的方法调用就是:objc_msgSendSuper(rw_objc_super,@selector(eat));结构体objc_super在objc源码里定义为:

struct objc_super{//这里是简化的写法
  id receiver;
  Class super_class;
}

objc_msgSendSuper参数的注释里解释,super_class只是告诉查找方法实现的时候是直接从父类的类对象开始查找;方法的接收者还是LYMWorker的实例;这里有总结两点:

  • 上述方法中调用的class方法在NSObject中实现,不论从哪个开始查找最终的实现都是在NSObject中的
  • class只是告诉类型是什么,类型至于方法的调用者有关(也就是消息接收者);因此[super class]只是告诉编译器从父类开始查找,所以最终返回的类型还是LYMWorker