阅读 109

MJiOS底层笔记--Runtime

本文属笔记性质,主要针对自己理解不太透彻的地方进行记录。

推荐系统直接学习小码哥iOS底层原理班---MJ老师的课确实不错,强推一波。


简介

  • 支撑Object-C的动态性
  • C语言提供结构,C\C++\汇编编写实现

isa

isa作为runtime底层中最常用的一个数据结构。

class的isa

在arm64架构之前,isa就是一个普通的指针,存储着Class、Meta-Class对象的内存地址

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;
复制代码

union的isa

从arm64架构开始,对isa进行了优化,变成了一个共用体(union)结构,使用位运算来获得更多的信息,但依旧是8位字符。

共用体与结构体类似,但内部所有成员将会共用(首元素的)内存。对整块内存通过位运算来进行扩展操作。

union isa_t 
{
    Class cls;
    uintptr_t bits; //决定了共用体所占的内存大小。可以使用位运算

# if __arm64__  //位运算用掩码 bits & MASK  \\ bits | MSAK
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
    // 位域负责可读性展示.从右向左
    struct {
        uintptr_t nonpointer        : 1; //占1位
        uintptr_t has_assoc         : 1;//占1位
        uintptr_t has_cxx_dtor      : 1;//占1位
        
        //由于这种运算除了中间33位都是0,所以arm64下,所有类对象地址的64位中除了这33位一定都是0
        uintptr_t shiftcls          : 33; //占33位  //class对象的地址  bits&ISA_MASK
        
        uintptr_t magic             : 6;//占6位
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 19;
#       define RC_ONE   (1ULL<<45)
#       define RC_HALF  (1ULL<<18)
    };
    
    .....
};
复制代码


class_wr_t

存放类信息的可读写列表

基本介绍可以查阅OC对象本质中关于class_wr_t的部分。

有一些东西不太重要的东西可以补充:

从源码上看,类在初始化时最开始的data() -> ro_t,在初始化过程中才将data() -> rw_t并且将ro_t赋值给rw_t -> ro_t


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; }
    };
};
复制代码

SEL

方法名,基本等同于C语言字符串 char*

typedef struct objc_selector *SEL;
复制代码

不同类中的同名方法,其SEL相同

types

函数返回值,参数类型

可以通过@encode(type)函数查看类型对应的编码

// "i24@0:8i16f20"  数字代表参数/返回值所占字节
// 0id 8SEL 16int 20float  == 24  
- (int)test:(int)age height:(float)height;
复制代码

需要注意的是,在构建方法签名(NSMethodSignature)时,即使简化成i@:if也是没有问题的。


cache_t

Class内部结构中有个方法缓存(cache_t),用散列表(哈希表)来缓存曾经调用过的方法,可以提高方法的查找速度

struct cache_t {
    struct bucket_t *_buckets; //散列表
    mask_t _mask; //散列表长度-1
    mask_t _occupied; //已缓存的方法数量
}
复制代码
struct bucket_t {
    cache_key_t _key;  //SEL
    IMP _imp; //IMP
}
复制代码

查找的顺序

本类 -> 父类

在其中任一位置查找到IMP,都将会写入本类(在查找到的那一刻,直接通过cls写入)缓存中。(父类本身则不会被缓存)

//LLDB中
p (IMP)0x0000123 可以打印内存地址所指的方法
复制代码

objc_msgSend

OC的消息机制。内部会经历消息发送、动态方法解析、消息转发三个阶段

OC中的方法调用,其实都是转换为objc_msgSend函数的调用

int main(int argc, const char * argv[]) {
    @autoreleasepool {
    
        MJPerson *person = [[MJPerson alloc] init];
        [person personTest];
        
        // 在编译是将会转化成
        objc_msgSend(person, @selector(personTest));

    }
    return 0;
}
复制代码

objc_msgSend底层,由汇编实现

ENTRY _objc_msgSend
	UNWIND _objc_msgSend, NoFrame
	MESSENGER_START
    //x0:寄存器 消息接收者 receiver
	cmp	x0, #0			// nil check and tagged pointer check
	
	//一旦消息接收者为0 跳转(b.le)到 LNilOrTagged
	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:
    //返回0
	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

	END_ENTRY _objc_msgSend
复制代码

objc_msgSend的消息机制,(在查找方法时)可以分为三个阶段:

消息发送、动态方法解析、消息转发。最后崩溃unrecognized selector sent to instance

消息发送阶段

在本类以及父类中查找方法

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

    ....
    ....
    ....
    
 retry:    
    runtimeLock.assertReading();
    // 查找本类缓存

    imp = cache_getImp(cls, sel);
    if (imp) goto done;

    // 查找本类方法列表
    {
        Method meth = getMethodNoSuper_nolock(cls, sel);
        if (meth) {
            log_and_fill_cache(cls, meth->imp, sel, inst, cls);
            imp = meth->imp;
            goto done;
        }
    }

    // 去查找父类
    {
        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;
            }
        }
    }

    ....
    ....
    ....
}
复制代码

几个源码方面的点

  1. objc_msgSend本身由汇编编写

在未查找到缓存后进入正常代码lookUpImpOrForward进行方法查找以及后续阶段操作。

  1. 本类会经历两次缓存查找

第一次是在汇编中直接查找缓存,第二次是在lookUpImpOrForward方法中查找本类re_t之前再查找一次。

  1. 方法列表的查找分为两种

已经排序的,二分查找。没有排序的,遍历查找

  1. 只要查询到方法存在,就会写入调用者类缓存。

  2. 所有父类查找完毕仍未找到方法。进入动态方法解析。

动态方法解析

如果未查找到指定方法,runtime允许开发者进行一次方法动态添加。

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

    ...
    ...
    ...
    
    
    // 进行一次动态解析`triedResolver`

    if (resolver  &&  !triedResolver) {
        runtimeLock.unlockRead();
        
        //内部会根据cls是否是元类调用不同的逻辑
        _class_resolveMethod(cls, sel, inst);
        runtimeLock.read();
        // 不会进行缓存
        // changed already. Re-do the search from scratch instead.
        triedResolver = YES;
        
        //重新走一遍方法查找,但如果还没有也不会再次被动态解析了。
        goto retry;
    }

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


    //消息转发
    ....
    ....

 done:
    runtimeLock.unlockRead();

    return imp;
}
复制代码
  1. resolveInstanceMethod的返回值其实没什么用

runtime里只做了打印返回值的工作而已

static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
    if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls, 
                         NO/*initialize*/, YES/*cache*/, NO/*resolver*/)) 
    {
        // Resolver not implemented.
        return;
    }

    BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
    bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);

    // Cache the result (good or bad) so the resolver doesn't fire next time.
    // +resolveInstanceMethod adds to self a.k.a. cls
    IMP imp = lookUpImpOrNil(cls, sel, inst, 
                             NO/*initialize*/, YES/*cache*/, NO/*resolver*/);

    if (resolved  &&  PrintResolving) {
        if (imp) {
            _objc_inform("RESOLVE: method %c[%s %s] "
                         "dynamically resolved to %p", 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel), imp);
        }
        else {
            // Method resolver didn't add anything?
            _objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
                         ", but no new implementation of %c[%s %s] was found",
                         cls->nameForLogging(), sel_getName(sel), 
                         cls->isMetaClass() ? '+' : '-', 
                         cls->nameForLogging(), sel_getName(sel));
        }
    }
}
复制代码
  1. 每次发送消息,未实现的都有一次动态解析的机会

消息转发

如果经历动态解析依旧没有确定方法。runtime允许开发者重新指定target,甚至修改方法调用的各种参数再次调用。

IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
                       bool initialize, bool cache, bool resolver)
{
    
    ....
    ....
    
    ....
    
    //查找失败,动态解析失败

    //进行消息转发.内部由汇编实现
    imp = (IMP)_objc_msgForward_impcache;
    cache_fill(cls, sel, imp, inst);

 done:
    runtimeLock.unlockRead();

    return imp;
}
复制代码
  1. forwardingTargetForSelector

如果forwardingTargetForSelector指定了一个新的target,会对其调用objc_msgSend,重新走一遍之前的逻辑。

  1. methodSignatureForSelector

有一种简便的调用方式,不需要自己编写type

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    if (aSelector == @selector(test:)) {
//        return [NSMethodSignature signatureWithObjCTypes:"v20@0:8i16"];
//        return [NSMethodSignature signatureWithObjCTypes:"i@:i"];
        return [[[MJCat alloc] init] methodSignatureForSelector:aSelector];
    }
    return [super methodSignatureForSelector:aSelector];
}
复制代码
  1. forwardInvocation

如果一个方法调用可以进入forwardingTargetForSelector,那么你可以对他进行任何操作。即使不invoke也不会出现崩溃。

  1. 类方法 OC中没有直接暴露消息转发的+方法,但是编写是可以调用,并且对类方法进行操作的。


int main(int argc, const char * argv[]) {
    @autoreleasepool {
        [MJPerson test];
        
        
    }
    return 0;
}


//因为是objc_msgSend是对MJPerson对象发送的消息,所以只有+方法才能被调用
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
    if (aSelector == @selector(test)) return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    
    return [super methodSignatureForSelector:aSelector];
}

+ (void)forwardInvocation:(NSInvocation *)anInvocation
{
    NSLog(@"1123");
}
复制代码
  1. 让类方法被实例对象调用

只要将target对象修改成实例对象即可

objc_msgSend并不区分类方法与对象方法,二者只有消息接受者的区别而已。

+ (id)forwardingTargetForSelector:(SEL)aSelector
{
    if (aSelector == @selector(test)) return [[MJCat alloc] init];
    return [super forwardingTargetForSelector:aSelector];
}
复制代码

super

super

@implementation MJStudent
- (void)run {
    [super run];
}

/cpp


struct objc_super {
    __unsafe_unretained _Nonnull id receiver; // 消息接收者
    __unsafe_unretained _Nonnull Class super_class; // 消息接收者的父类
};


static void _I_MJStudent_run(MJStudent * self, SEL _cmd) {
    
    ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("MJStudent"))}, sel_registerName("run"));
    
    //化简一下
    objc_msgSendSuper((__rw_objc_super){
                        (id)self, 
                        (id)class_getSuperclass(objc_getClass("MJStudent"))}, 
                        sel_registerName("run"));
                        


    //继续化简
    struct objc_super arg = {self, [MJPerson class]};
    objc_msgSendSuper(arg, @selector(run));
}
复制代码

可见在进行super调用时,使用的是objc_msgSendSuper方法。在objc_super结构体内部的receiver依旧是self,只是在查找IMP时,从父类开始查找IMP实现而已。


isMemberOfClass && isKindOfClass

对象方法是比对[self class],类方法是比对object_getClass(self)

所以在对类对象使用isMemberOfClass或者isKindOfClass时,需要对元类进行对比。 不过对于元类NSObject,其父类是NSObject所以会有点不同结果。

- (BOOL)isMemberOfClass:(Class)cls {
    return [self class] == cls;
}

- (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}


+ (BOOL)isMemberOfClass:(Class)cls {
    return object_getClass((id)self) == cls;
}


+ (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}
@end

复制代码

结果如下:

// 这句代码的方法调用者不管是哪个类(只要是NSObject体系下的),都返回YES
NSLog(@"%d", [NSObject isKindOfClass:[NSObject class]]); // 1

NSLog(@"%d", [NSObject isMemberOfClass:[NSObject class]]); // 0
NSLog(@"%d", [MJPerson isKindOfClass:[MJPerson class]]); // 0
NSLog(@"%d", [MJPerson isMemberOfClass:[MJPerson class]]); // 0
复制代码

方法交换

交换两个方法的IMP,并且清空cache

void method_exchangeImplementations(Method m1, Method m2)
{
    if (!m1  ||  !m2) return;

    rwlock_writer_t lock(runtimeLock);

    IMP m1_imp = m1->imp;
    m1->imp = m2->imp;
    m2->imp = m1_imp;


    // RR/AWZ updates are slow because class is unknown
    // Cache updates are slow because class is unknown
    // fixme build list of classes whose Methods are known externally?

    flushCaches(nil);//清空类对象的方法缓存

    updateCustomRR_AWZ(nil, m1);
    updateCustomRR_AWZ(nil, m2);
}

复制代码

LLVM编译器的中间代码

现在的LLVM编译器在将代码转成汇编之前,不再由CPP进行过度。而改为通过一种LLVM编译器专属的语言。

可以使用以下命令行指令生成中间代码

// 不需要指定架构
clang -emit-llvm -S main.m
复制代码

之后会生成一个.ll文件。

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
    [super forwardInvocation:anInvocation];
    
    int a = 10;
    int b = 20;
    int c = a + b;
    test(c);
}



define internal void @"\01-[MJPerson forwardInvocation:]"(%0*, i8*, %1*) #1 {
  %4 = alloca %0*, align 8
  %5 = alloca i8*, align 8
  %6 = alloca %1*, align 8
  %7 = alloca %struct._objc_super, align 8
  %8 = alloca i32, align 4
  %9 = alloca i32, align 4
  %10 = alloca i32, align 4
  store %0* %0, %0** %4, align 8
  store i8* %1, i8** %5, align 8
  store %1* %2, %1** %6, align 8
  %11 = load %0*, %0** %4, align 8
  %12 = load %1*, %1** %6, align 8
  %13 = bitcast %0* %11 to i8*
  %14 = getelementptr inbounds %struct._objc_super, %struct._objc_super* %7, i32 0, i32 0
  store i8* %13, i8** %14, align 8
  %15 = load %struct._class_t*, %struct._class_t** @"OBJC_CLASSLIST_SUP_REFS_$_", align 8
  %16 = bitcast %struct._class_t* %15 to i8*
  %17 = getelementptr inbounds %struct._objc_super, %struct._objc_super* %7, i32 0, i32 1
  store i8* %16, i8** %17, align 8
  %18 = load i8*, i8** @OBJC_SELECTOR_REFERENCES_, align 8, !invariant.load !8
  call void bitcast (i8* (%struct._objc_super*, i8*, ...)* @objc_msgSendSuper2 to void (%struct._objc_super*, i8*, %1*)*)(%struct._objc_super* %7, i8* %18, %1* %12)
  store i32 10, i32* %8, align 4
  store i32 20, i32* %9, align 4
  %19 = load i32, i32* %8, align 4
  %20 = load i32, i32* %9, align 4
  %21 = add nsw i32 %19, %20
  store i32 %21, i32* %10, align 4
  %22 = load i32, i32* %10, align 4
  call void @test(i32 %22)
  ret void
}
复制代码

一些语法

@ - 全局变量
% - 局部变量
alloca - 在当前执行的函数的堆栈帧中分配内存,当该函数返回到其调用者时,将自动释放内存
i32 - 32位4字节的整数
align - 对齐
load - 读出,store 写入
icmp - 两个整数值比较,返回布尔值
br - 选择分支,根据条件来转向label,不根据条件跳转的话类似 goto
label - 代码标签
call - 调用函数
复制代码

一些实操注意点

利用关联对象(AssociatedObject)给分类添加属性

遍历类的所有成员变量(修改textfield的占位文字颜色、字典转模型、自动归档解档)

交换方法实现(交换系统的方法)

利用消息转发机制解决方法找不到的异常问题

如何给int类型的变量赋值

MJPerson *person = [[MJPerson alloc] init];
object_setIvar(person, nameIvar, @"123");
//先将10,转化为指针类变量指向10这个值。再将指针转化成id
object_setIvar(person, ageIvar, (__bridge id)(void *)10); 
NSLog(@"%@ %d", person.name, person.age);
复制代码

几个LLDB命令

关注下面的标签,发现更多相似文章
评论