阅读 896

RunTime实现原理剖析

对于RunTime恐怕几乎每一个做iOS的人都听说过,都用过吧,但是对于其具体实现好多人应该都不太清楚吧,今天我这分4部分,详细的讲解一下Runtime,让大家对Runtime有一个全局的了解

  • 1、isa解析
  • 2、方法缓存
  • 3、objc_msgSend执行流程
  • 4、RunTime的相关API

isa指针

我们在研究OC对象的时候已经知道了,实力对象的isa指向类对象,类对象的isa指向元类对象。其实这样说还是有一点不对的,应该说在arm64架构之前,isa就是一个普通的指针,存储着ClassMeta-Class 对象的内存地址;但是从arm64之后,对isa进行了优化,变成了一个共用体(union)结构,还使用位域来存放跟多的信息。

我们在这里下载runtime源码,然后查找struct objc_object里面的isa,这里我们只研究arm64架构isa

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;
#       define RC_ONE   (1ULL<<45)
#       define RC_HALF  (1ULL<<18)
};
复制代码

我们发现isa的结构是这种共用体(union)结构,其实使用这种共用体是一种优化,isa不在单独存放的是一个指针信息了,里面存放了更多的其他信息。

概念

想要明白isa变成共用体(union)结构,是一种优化,我们需要先了解一些概念

  • 1、位运算
  • 2、字节和位
  • 3、位域
  • 4、共用体

位运算

位运算的运算符有下面几个

  • 1、左移:<<
  • 2、右移:>>
  • 3、按位或:|
  • 4、按位与:&
  • 5、按位取反:~
  • 6、按位异或:^ 其功能是参与运算的两数各对应的二进位相异或,当两对应的二进位相异时,结果为1

与操作& 与操作&:都是1则为1,一个0就是0。可以用来取出来特定的位。例如一个二进制0b 0000 0111,我们分别想取出第一位1和第四位0

 0000 0111            0000 0111
&0000 0001           &0000 1000
--------------       --------------
 0000 0001            0000 0000
复制代码

我们可以发现我们使用按位与&的时候,我们如果想取出哪一位,把改为设置为1,其他位设置为0就可以了。

介绍到了&,我再来介绍一个概念,掩码:一般用来按位与(&)运算的,具体有什么作用,我们下面会进行讲解

或操作|

或操作|:一个是1,则为1,全部是0才为0。 例如一个二进制0b 0101 1010

  0101 1010
| 0001 1100           
--------------
  0101 1110
复制代码

如果我们想要某一位,就该该位或上一个0

左移:<< 二进制位全部左移若干位,左边的丢弃,右边补0

  • 1、1<<0 1左移0位,0b0000 0001
  • 2、1<<1 1左移1位,0b0000 0010
  • 3、1<<2 1左移2位,0b0000 0100
  • 4、1<<3 1左移3位,0b0000 1000

右移:>>

二进制右移若干位,正数左边补0,负数左边补1,右边丢弃。

例如 12>>2

0000 1100 = 12

0000 0011 = 3 (右移后)

特点:每右移一位,就除以一次2。a>>n 就是 a除以2的n次方

字节和位

  • Bit意为“位”,是计算机运算的基础,属于二进制的范畴;
  • Byte意为“字节”,是计算机文件大小的基本计算单位;

通常用bit来作数据传输的单位,因为物理层,数据链路层的传输对于用户是透明的,而这种通信传输是基于二进制的传输。在应用层通常是用byte来作单位,表示文件的大小,在用户看来就是可见的数据大小

换算 1 Byte = 8 Bits 1 KB = 1024 Bytes 1 MB = 1024 KB 1 GB = 1024 MB 另外,Byte通常简写为B(大写),而bit通常简写为b(小写)。可以这么记忆,大写的为大单位,实际数值小,小写的为小单位,实际数值较大,1B=8b。

位域

所谓”位域“是把一个字节中的二进位划分为几 个不同的区域, 并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。它实际上是C语言提供的一种数据结构。

使用位域的好处是:

  • 1.有些信息在存储时,并不需要占用一个完整的字节, 而只需占几个或一个二进制位。例如在存放一个开关量时,只有0和1 两种状态, 用一位二进位即可。这样节省存储空间,而且处理简便。 这样就可以把几个不同的对象用一个字节的二进制位域来表示。
  • 2.可以很方便的利用位域把一个变量给按位分解。比如只需要4个大小在0到3的随即数,就可以只rand()一次,然后每个位域取2个二进制位即可,省时省空间

struct 位域结构名 { 位域列表 }; 其中位域列表的形式为: 类型说明符 位域名:位域长度;

struct {
char tall : 1;
char rich : 1;
char handsome : 1;
} _tallRichHandsome;
复制代码

4、共用体

union中可以定义多个成员,union的大小由最大的成员的大小决定

union成员共享同一块大小的内存,一次只能使用其中的一个成员; 对union某一个成员赋值,会覆盖其他成员的值(但前提是成员所占字节数相同,当成员所占字节数不同时只会覆盖相应字节上的值,比如对char成员赋值就不会把整个int成员覆盖掉,因为char只占一个字节,而int占四个字节); union量的存放顺序是所有成员都从低地址开始存放的。

案例

例如我们创建一个Person类,里面有三个Bool属性,tallrichhandsome

@property (nonatomic,assign) BOOL tall;
@property (nonatomic,assign) BOOL rich;
@property (nonatomic,assign) BOOL handsome;
复制代码

我们知道这三个属性占用了3个字节。其实这个时候我们可以考虑到使用位域或者共用体的概念,使用位(Bit)的0和1来代表这三个属性的YES NO,那个三个属性就只是占用了2个字节

位域代码

@interface Person()
{
// 位域
struct {
char tall : 1;
char rich : 1;
char handsome : 1;
} _tallRichHandsome;
}
@end
@implementation Person

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

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

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

- (BOOL)isRich
{
return !!_tallRichHandsome.rich;
}

- (void)setHandsome:(BOOL)handsome
{
_tallRichHandsome.handsome = handsome;
}

- (BOOL)isHandsome
{
return !!_tallRichHandsome.handsome;
}
复制代码

为什么会出现!!,我们知道!(-1) == NO!上一个存在的值是NO!!两次那么只会出现YES 和 NO了。

共用体

其实我们观察isa的类型,发现isa其实是使用的共用体

#define TallMask (1<<0)
#define RichMask (1<<1)
#define HandsomeMask (1<<2)


@interface Person()
{
union {
int bits;
struct {
char tall : 1;
char rich : 1;
char handsome : 1;
};
} _tallRichHandsome;
}
@end

@implementation Person

- (void)setTall:(BOOL)tall
{
if (tall) {
_tallRichHandsome.bits |= TallMask;
} else {
_tallRichHandsome.bits &= ~TallMask;
}
}

- (BOOL)isTall
{
return !!(_tallRichHandsome.bits & TallMask);
}

- (void)setRich:(BOOL)rich
{
if (rich) {
_tallRichHandsome.bits |= RichMask;
} else {
_tallRichHandsome.bits &= ~RichMask;
}
}

- (BOOL)isRich
{
return !!(_tallRichHandsome.bits & RichMask);
}

- (void)setHandsome:(BOOL)handsome
{
if (handsome) {
_tallRichHandsome.bits |= HandsomeMask;
} else {
_tallRichHandsome.bits &= ~HandsomeMask;
}
}

- (BOOL)isHandsome
{
return !!(_tallRichHandsome.bits & HandsomeMask);
}
复制代码

#define TallMask (1<<0)这是掩码,为了方便阅读。

struct {
char tall : 1;
char rich : 1;
char handsome : 1;
};
复制代码

其实也仅仅是方便阅读的作用,让我们知道tallrichhandsome是在哪一位上,去掉并不影响代码。

扩展:位运算应用

其实我们可以看到苹果官方文档上面有很多地方运用到了位运算

typedef NS_ENUM(NSInteger, LXDAuthorizationType)
{
LXDAuthorizationTypeNone = 0,
LXDAuthorizationTypePush = 1 << 0,  ///<    推送授权
LXDAuthorizationTypeLocation = 1 << 1,  ///<    定位授权
LXDAuthorizationTypeCamera = 1 << 2,    ///<    相机授权
LXDAuthorizationTypePhoto = 1 << 3,     ///<    相册授权
LXDAuthorizationTypeAudio = 1 << 4,  ///<    麦克风授权
LXDAuthorizationTypeContacts = 1 << 5,  ///<    通讯录授权
};

复制代码
typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {
UIViewAutoresizingNone                 = 0,
UIViewAutoresizingFlexibleLeftMargin   = 1 << 0,
UIViewAutoresizingFlexibleWidth        = 1 << 1,
UIViewAutoresizingFlexibleRightMargin  = 1 << 2,
UIViewAutoresizingFlexibleTopMargin    = 1 << 3,
UIViewAutoresizingFlexibleHeight       = 1 << 4,
UIViewAutoresizingFlexibleBottomMargin = 1 << 5
};
复制代码

太多了,我就不一一列举了。其实我们在有些情况下也可以参考这样的设计。 例如

typedef enum {
OptionsOne = 1<<0,   // 0b0001
OptionsTwo = 1<<1,   // 0b0010
OptionsThree = 1<<2, // 0b0100
OptionsFour = 1<<3   // 0b1000
} Options

- (void)setOptions:(Options)options
{
if (options & OptionsOne) {
NSLog(@"包含了OptionsOne");
}

if (options & OptionsTwo) {
NSLog(@"包含了OptionsTwo");
}

if (options & OptionsThree) {
NSLog(@"包含了OptionsThree");
}

if (options & OptionsFour) {
NSLog(@"包含了OptionsFour");
}
}


调用上面方法
[self setOptions: OptionsOne | OptionsFour];

复制代码

总结

isa.png

最后我们在看一下isa结构吧

  • 1、nonpointer:0,代表普通的指针,存储着Class、Meta-Class对象的内存地址;1,代表优化过,使用位域存储更多的信息
  • 2、has_assoc:是否有设置过关联对象,如果没有,释放时会更快
  • 4、shiftcls:存储着Class、Meta-Class对象的内存地址信息
  • 5、magic:用于在调试时分辨对象是否未完成初始化
  • 6、weakly_referenced:是否有被弱引用指向过,如果没有,释放时会更快
  • 7、deallocating:对象是否正在释放
  • 8、extra_rc:里面存储的值是引用计数器
  • 9、has_sidetable_rc:引用计数器是否过大无法存储在isa中;如果为1,那么引用计数会存储在一个叫SideTable的类的属性中

第三条解释不知道为啥违反政治安全问题了,不让写,只能截图了

方法缓存

我们先来整体的看一下结构

方法缓存.png

  • 1、class类中只要有isa指针superClasscache方法缓存bits具体的类信息
  • 2、bits & FAST_DATA_MASK 指向一个新的结构体Class_rw_t,里面包含着methods方法列表properties属性列表protocols协议列表class_ro_t类的初始化信息等一些类信息

Class_rw_t Class_rw_t里面的methods方法列表properties属性列表都是二维数组,是可读可写的,包含类的初始内容分类的内容

方法缓存1.png

class_ro_t

class_ro_t里面的baseMethodList,baseProtocols,Ivars,baseProperties是一维数组,是只读的,包含类的初始化内容

方法缓存2.png

method_t

method_t是对方法的封装

struct method_t{
SEL name;//函数名
const char *types;//编码(返回值类型,参数类型)
IMP imp;//指向函数的指针(函数地址)
}
复制代码

IMP

IMP代表函数的具体实现

typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...); 
复制代码

第一个参数是指向self的指针(如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针),第二个参数是方法选择器(selector)

SEL

SEL代表方法名,一般叫做选择器,底层结构跟char *类似

  • 可以通过@selector()sel_registerName()获得
  • 可以通过sel_getName()NSStringFromSelector()转成字符串
  • 不同类中相同名字的方法,所对应的方法的选择器是相同的
  • 具体实现typedef struct objc_selector *SEL

types

types包含了函数返回值,参数编码的字符串

结构为:返回值 参数1 参数2...参数N

iOS中提供了一个叫做@encode的指令,可以将具体的类型表示成字符串编码

方法缓存3.png

例如

// "i24@0:8i16f20"
// 0id 8SEL 16int 20float  == 24
- (int)test:(int)age height:(float)height
复制代码

每一个方法都有两个默认参数self_msg 我们可以查到id类型为@SEL类型为:

  • 1、第一个参数i返回值
  • 2、第二个参数@id 类型的self
  • 3、第三个参数:SEL 类型的_msg
  • 4、第四个参数iInt age
  • 5、第五个参数ffloat height

其中加载的数字其实是跟所占字节有关

  • 1、24 总共占有多少字节
  • 2、@0id 类型的self的起始位置为0
  • 3、:8 是因为id 类型的self占字节为8,所以SEL 类型的_msg`的起始位置为8

方法缓存

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

方法缓存4.png

cache_t结构体里面有三个元素

  • buckets 散列表,是一个数组,数组里面的每一个元素就是一个bucket_t,bucket_t里面存放两个

    • _key SEL作为key
    • _imp 函数的内存地址
  • _mask 散列表的长度

  • _occupied已经缓存的方法数量

为什么会用到方法缓存

image.png

这张图片是我们方法产找路径,如果我们的一个类有多个父类,需要调用父类方法,他的查找路径为

  • 1、先遍历自己所有的方法
  • 2、如果在自己类中找不到方法,则遍历父类所有方法,没有查找到调用方法之前,一直重复该动作 如果每一次方法调用都是走这样的步骤,对于系统级方法来说,其实还是比较消耗资源的,为了应对这个情况。出现了方法缓存,调用过的方法,都放在缓存列表中,下次查找方法的时候,现在缓存中查找,如果缓存中查找不到,然后在执行上面的方法查找流程。

散列表结构

方法缓存5.png

散列表的结构大概就像上面那样,数组的下标是通过@selector(方法名)&_mask来求得,具体每一个数组的元素是一个结构体,里面包含两个元素_imp@selector(方法名)作为的key

我们在上一篇文章中知道,一个值与&上一个_mask,得出的结果一定小于等于_mask值,而_mask值为数组长度-1,所以任何时候,也不会越界。

其实这就是散列表的算法,也有一些其他的算法,取余,一个值取余&的效果是相同的。

但是这其实是有几个疑虑的

  • 1、初始_mask是多少? - 初始_mask我简单了尝试了一下,第一次可能给3
  • 2、随着方法的增加,方法数量超过_mask值了怎么办 - 随着方法的增多,方法数量肯定会超过_mask,这个时候会清空缓存散列表,然后_mask*2
  • 3、如果两个值&_mask的值相同了怎么办 - 如果两个值&_mask的值相同时,第二个&减一,知道找到空值,如果减到0还没有找到空位置,那就放在最大位置
  • 4、在没有存放cach_t的数组位置怎么处理
    • 在没有占用是,会在空位置的值为NULL

源码查看 我们在objc-cache.mm文件中查找bucket_t * cache_t::find(cache_key_t k, id receiver)方法。

bucket_t * cache_t::find(cache_key_t k, id receiver)
{
assert(k != 0);

bucket_t *b = buckets();
mask_t m = mask();
mask_t begin = cache_hash(k, m);
mask_t i = begin;
do {
if (b[i].key() == 0  ||  b[i].key() == k) {
return &b[i];
}
} while ((i = cache_next(i, m)) != begin);

// hack
Class cls = (Class)((uintptr_t)this - offsetof(objc_class, cache));
cache_t::bad_cache(receiver, (SEL)k, cls);
}
复制代码

计算index值

mask_t begin = cache_hash(k, m);
复制代码

这个方式是计算下标的,我们点击进入查看具体实现,就是@selector(方法名)&_mask

方法缓存6.png

当两个值求的下标相同时

(i = cache_next(i, m)) != begin
复制代码

具体实现为

方法缓存7.png

arm64x86实现方法不一样

这里有一个MJ老师封装的能够查看对象各种属性的方法,想要使用的可以在这里查看

方法缓存8.png

objc_msgSend执行流程

OC中的方法调用,其实都是转化为objc_msgSend函数的调用,objc_msgSend的执行流程可以分为3大阶段

  • 1、消息发送
  • 2、动态方法解析
  • 3、消息转发

消息发送

消息发送1.png

消息发送流程是我们平时最经常使用的流程,其他的像动态方法解析消息转发其实是补救措施。具体流程如下

  • 1、首先判断消息接受者receiver是否为nil,如果为nil直接退出消息发送
  • 2、如果存在消息接受者receiverClass,首先在消息接受者receiverClasscache中查找方法,如果找到方法,直接调用。如果找不到,往下进行
  • 3、没有在消息接受者receiverClasscache中找到方法,则从receiverClassclass_rw_t中查找方法,如果找到方法,执行方法,并把该方法缓存到receiverClasscache中;如果没有找到,往下进行
  • 4、没有在receiverClass中找到方法,则通过superClass指针找到superClass,也是现在缓存中查找,如果找到,执行方法,并把该方法缓存到receiverClasscache中;如果没有找到,往下进行
  • 5、没有在消息接受者superClasscache中找到方法,则从superClassclass_rw_t中查找方法,如果找到方法,执行方法,并把该方法缓存到receiverClasscache中;如果没有找到,重复4、5步骤。如果找不到了superClass了,往下进行
  • 6、如果在最底层的superClass也找不到该方法,则要转到动态方法解析

动态方法解析

消息发送2.png

  • 开发者可以实现以下方法,来动态添加方法实现

    • +resolveInstanceMethod:
    • +resolveClassMethod:
  • 动态解析过后,会重新走“消息发送”的流程,从receiverClass的cache中查找方法这一步开始执行

我们创建一个Person类,然后在.h文件中写一个- (void)test,但是不写具体实现,然后调用。会打印出最常见的unrecognized selector sent to instance 0x100559b60

动态方法解析1

动态方法解析需要调用resolveInstanceMethod或者resolveClassMethod一个对应实例方法,一个对应类方法。我们这里是实例方法使用resolveInstanceMethod

我们看一下resolveInstanceMethod的解释,在我们需要执行动态方法解析的时候我们最好返回YES

消息发送3.png

- (void)other{
NSLog(@"%s",__func__);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(test)) {
//获取其他方法
Method method = class_getInstanceMethod(self, @selector(other));
//动态添加test的方法
class_addMethod(self, sel, method_getImplementation(method), method_getTypeEncoding(method));
}

return [super resolveInstanceMethod:sel];
}

@end
复制代码

class_addMethod方法中我们需要imptypes,但是OC并没有提供相关属性,所有我们可以调用相关方法来获取相关参数

消息发送4.png

动态方法解析2

这里我们在随便验证一下method的结构是不是这种

struct method_t {
SEL sel;
char *types;
IMP imp;
};
复制代码

我们代码改成这样

struct method_t {
SEL sel;
char *types;
IMP imp;
};
+ (BOOL)resolveInstanceMethod:(SEL)sel{

if (sel == @selector(test)) {
//获取其他方法
struct method_t *method = (struct method_t *)class_getInstanceMethod(self, @selector(other));
//动态添加test的方法
class_addMethod(self, sel, method->imp, method->types);

return  YES;
}

return [super resolveInstanceMethod:sel];
}
复制代码

消息发送5.png

动态方法解析3

其实我们还可以用C语言验证一下,提示:C语言中函数方法就是函数的地址

void c_other(id self, SEL _cmd)
{
NSLog(@"c_other - %@ - %@", self, NSStringFromSelector(_cmd));
}
+ (BOOL)resolveInstanceMethod:(SEL)sel{

if (sel == @selector(test)) {

class_addMethod(self, sel, (IMP)c_other, "v16@0:8");
return YES;
}

return [super resolveInstanceMethod:sel];
}

复制代码

消息转发

如果方法一个方法在消息发送阶段没有找到相关方法,也没有进行动态方法解析,这个时候就会走到消息转发阶段了。

消息发送6.png

  • 调用forwardingTargetForSelector,返回值不为nil时,会调用objc_msgSend(返回值, SEL)
  • 调用methodSignatureForSelector,返回值不为nil,调用forwardInvocation:方法;返回值为nil时,调用doesNotRecognizeSelector:方法
  • 开发者可以在forwardInvocation:方法中自定义任何逻辑
  • 以上方法都有对象方法、类方法2个版本(前面可以是加号+,也可以是减号-)

forwardingTargetForSelector

我们创建一个命令行项目,创建两个类,personStudent,在person.h里面写一个实例方法,但是不去实现相关方法。

@interface Person : NSObject
- (void)test;
@end


@interface Student : NSObject
- (void)test;
@end
#import "Student.h"

@implementation Student
- (void)test{
NSLog(@"%s",__func__);
}
@end
复制代码

调用的时候回报出我们最常见的错误unrecognized selector sent to instance 0x100747a50

如果我们在person里面实现这个方法

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

消息发送7.png

调用forwardingTargetForSelector,返回值不为nil时,会调用objc_msgSend(返回值, SEL),结果就是调用了objc_msgSend(Student,test)

methodSignatureForSelector(方法签名)

forwardingTargetForSelector返回值为nil,或者都没有调用该方法的时候,系统会调用methodSignatureForSelector方法。调用methodSignatureForSelector,返回值不为nil,调用forwardInvocation:方法;返回值为nil时,调用doesNotRecognizeSelector:方法

对于方法签名的生成方式

  • 1、[NSMethodSignature signatureWithObjCTypes:"i@:i"]
  • 2、[[[Student alloc]init] methodSignatureForSelector:aSelector];

实现方法签名以后我们还要实现forwardInvocation方法,当调用persontest的方法的时候,就会走到这个方法中

消息发送8.png

NSInvocation封装了一个方法调用,包括:方法调用者、方法名、方法参数

  • anInvocation.target 方法调用者
  • anInvocation.selector 方法名
  • [anInvocation getArgument:NULL atIndex:0]

我们也可以先执行NSLog(@"========");在执行Student的test方法

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
NSLog(@"========");
anInvocation.target = [[Student alloc]init];
[anInvocation invoke];

//    [anInvocation invokeWithTarget:[[Student alloc] init]];
}
复制代码

消息发送9.png

其中这两个方法是一样的 [anInvocation invokeWithTarget:[[Student alloc] init]];

anInvocation.target = [[Student alloc]init];
[anInvocation invoke];
复制代码

其实这个方法还是比较有用的,像网上一些对bug处理都会用到这个方法

RunTime的相关API

类方法

  • 1、Class objc_allocateClassPair(Class superclass, const char *name, size_t extraBytes) 动态创建一个类(参数:父类,类名,额外的内存空间)
  • 2、void objc_registerClassPair(Class cls) 注册一个类(要在类注册之前添加成员变量)
  • 3、void objc_disposeClassPair(Class cls) 销毁一个类
  • 4、Class object_getClass(id obj) 获取isa指向的Class
  • 5、Class object_setClass(id obj, Class cls) 设置isa指向的Class
  • 6、BOOL object_isClass(id obj) 判断一个OC对象是否为Class
  • 7、BOOL class_isMetaClass(Class cls) 判断一个Class是否为元类
  • 8、Class class_getSuperclass(Class cls)获取父类

我在方法缓存讲过,在创建一个实例对象以后,里面的成员变量就固定了,不能在修改了。因此我们在用objc_registerClassPair注册类的时候,我们必须把成员变量写在注册之前。 简单使用,因为这里面的都是runtime底层方法写的,所有点语法和set方法都不可以使用,如果想要遍历里面的属性和方法还是需要使用runtime提供的方法

创建类

// 创建类
Class newClass = objc_allocateClassPair([NSObject class], "MJDog", 0);
class_addIvar(newClass, "_age", 4, 1, @encode(int));
class_addIvar(newClass, "_weight", 4, 1, @encode(int));
//注册类
objc_registerClassPair(newClass);

// 成员变量的数量
unsigned int count;
Ivar *ivars = class_copyIvarList(newClass, &count);
for (int i = 0; i < count; i++) {
// 取出i位置的成员变量
Ivar ivar = ivars[i];
NSLog(@"%s %s", ivar_getName(ivar), ivar_getTypeEncoding(ivar));
}
free(ivars);

// 在不需要这个类时释放
  objc_disposeClassPair(newClass);
复制代码

设置isa指向的Class

Person *p = [[Person alloc]init];
object_setClass(p, [Cat class]);
NSLog(@"%@",p);
复制代码

image.png

成员变量

  • 1、Ivar class_getInstanceVariable(Class cls, const char *name) 获取一个实例变量信息
  • 2、Ivar *class_copyIvarList(Class cls, unsigned int *outCount) 拷贝实例变量列表(最后需要调用free释放)
  • 3、void object_setIvar(id obj, Ivar ivar, id value) 设置成员变量的值
  • 4、id object_getIvar(id obj, Ivar ivar) 获取成员变量的值
  • 5、BOOL class_addIvar(Class cls, const char * name, size_t size, uint8_t alignment, const char * types) 动态添加成员变量(已经注册的类是不能动态添加成员变量的)
  • 6、const char *ivar_getName(Ivar v), const char *ivar_getTypeEncoding(Ivar v)获取成员变量的相关信息

最常用的方法就是获取类的成员变量

unsigned int count;
Ivar *ivars = class_copyIvarList([Person class], &count);
for (int i = 0; i < count; i++) {
// 取出i位置的成员变量
Ivar ivar = ivars[i];
NSLog(@"%s %s", ivar_getName(ivar), ivar_getTypeEncoding(ivar));
}
free(ivars);

复制代码

常用的方案

  • 1、JSON转Model
  • 2、常看写控件都有哪些元素,然后进行修改

image.png

属性

  • 1、objc_property_t class_getProperty(Class cls, const char *name) 获取一个属性

  • 2、objc_property_t *class_copyPropertyList(Class cls, unsigned int *outCount)拷贝属性列表(最后需要调用free释放)

  • 3、BOOL class_addProperty(Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount) 动态添加属性

  • 4、void class_replaceProperty(Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount) 动态替换属性

  • 5、const char *property_getName(objc_property_t property) 获取属性的一些信息

  • 6、const char *property_getAttributes(objc_property_t property) 获取属性的一些信息

    方法

  • 1、获得一个实例方法、类方法 - Method class_getInstanceMethod(Class cls, SEL name) - Method class_getClassMethod(Class cls, SEL name)

  • 2、方法实现相关操作 - IMP class_getMethodImplementation(Class cls, SEL name) - IMP method_setImplementation(Method m, IMP imp) - void method_exchangeImplementations(Method m1, Method m2)

  • 3、拷贝方法列表(最后需要调用free释放)

    • Method *class_copyMethodList(Class cls, unsigned int *outCount)
  • 4、动态添加方法

    • BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
  • 5、动态替换方法

    • IMP class_replaceMethod(Class cls, SEL name, IMP imp, const char *types)
  • 6、选择器相关

    • const char *sel_getName(SEL sel)
    • SEL sel_registerName(const char *str)
  • 7、用block作为方法实现

    • IMP imp_implementationWithBlock(id block)
    • id imp_getBlock(IMP anImp)
    • BOOL imp_removeBlock(IMP anImp)

最常见的就是动态方法交换

Method runMethod = class_getInstanceMethod([Person class], @selector(run));
Method testMethod = class_getInstanceMethod([Person class], @selector(test));
method_exchangeImplementations(runMethod, testMethod)
复制代码

还有一个方法替换

MJPerson *person = [[Person alloc] init];

//        class_replaceMethod([Person class], @selector(run), (IMP)myrun, "v");


class_replaceMethod([Person class], @selector(run), imp_implementationWithBlock(^{
NSLog(@"123123");
}), "v");

[person run];
复制代码

我们经常会看一些面试题,但是好多面试题我们都是知其然不知其所以然,你如果认真的看了我上面总结的几十篇文章,那么你也会知其所以然。

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