Runtime

Objective-C Runtime能做什么?

发布于:2014-05-27 13:53阅读数:6332

在之前的文章中我们介绍了Runtime是什么,属于理论性介绍,你看了上篇很迫切的想知道Runtime到底能干什么?表着急,这一篇Blog将将讲解Runtime怎么应用到实战中Runtime官方文档在这里,包括了接

 

转自Anselz的博客

之前的文章中我们介绍了Runtime是什么,属于理论性介绍,你看了上篇很迫切的想知道Runtime到底能干什么?不要着急,这一篇Blog将将讲解Runtime怎么应用到实战中Runtime官方文档在这里,包括了接口名字以及使用说明。下文讲到的接口都能在此文档中找到。
 
KVC中setValue中使用
我们知道在KVC中如果直接setValue如果对象没有这个属性或者是变量就会直接Crash,如:
  1. RuntimeObj *obj = [[RuntimeObj alloc]init]; 
  2. [obj setValue:@"value4Name" forKey:@"objName"];//RuntimeObj 没有objName这个属性 
这段代码会直接Crash
 
有没有对这个感觉头疼,要是能先用某种方式检查下再set那就不会Crash? 没错,这件事情Runtime能做到
 
先看一下示例代码吧:
  1. -(BOOL)hasAttribute:(NSString *)attName 
  2.     BOOL flag = NO; 
  3.     u_int               count; 
  4.     Ivar *ivars = class_copyIvarList([self class], &count); 
  5.     for (int i = 0; i < count ; i++) 
  6.     { 
  7.         const char* propertyName = ivar_getName(ivars[i]); 
  8.         NSString *strName = [NSString  stringWithCString:propertyName encoding:NSUTF8StringEncoding]; 
  9.         if ([attName isEqualToString:strName]) { 
  10.             flag = YES; 
  11.         } 
  12.         NSLog(@"===%@",strName); 
  13.     } 
  14.     return flag; 
没错,这个函数就是能帮你检查是否有某个属性或变量,下面讲解下一个代码:
Ivar 原型是typedef struct objc_ivar *Ivar; 
 
class_copyIvarList 返回的是某个类所有属性或变量原型Ivar *class_copyIvarList(Class cls, unsigned int *outCount) ;
 
ivar_getName 返回的是没有 Ivar 结构体的名字,即变量的名字 原型 const char *ivar_getName(Ivar v);
 
* 与这个对应的还有一个函数class_copyPropertyListclass_copyIvarList 不同点在前者只取属性(@property申明的属性) 后者所有的 包括在interface大括号中申明的。
 
class_copyPropertyList使用的示例代码如下:
  1. objc_property_t*    properties= class_copyPropertyList([self class], &count); 
  2. for (int i = 0; i < count ; i++) 
  3.      const char* propertyName = property_getName(properties[i]); 
  4.      NSString *strName = [NSString  stringWithCString:propertyName encoding:NSUTF8StringEncoding]; 
  5.       NSLog(@"===%@",strName); 
  
两个不同可以用代码来演示的,具体代码自己动手写 我就不贴出来,看看两者到底有什么区别?
 
有了这一步 你还担心滥用KVC时崩溃了么?
 
动态创建函数
有时候会根据项目需求动态创建某个函数,没错Runtime完全能做到
 
先看代码:
  1. void dynamicMethod(id self, SEL _cmd) 
  2.     printf("SEL %s did not exist ",sel_getName(_cmd)); 
  3.   
  4. + (BOOL) resolveInstanceMethod:(SEL)aSEL 
  5.     
  6.     class_addMethod([self class], aSEL, (IMP)dynamicMethod, "v@:"); 
  7.     return YES; 
  8. void dynamicMethod(id self, SEL _cmd) 
  9.     printf("SEL %s did not exist ",sel_getName(_cmd)); 
  10.   
  11. + (BOOL) resolveInstanceMethod:(SEL)aSEL 
  12.     class_addMethod([self class], aSEL, (IMP)dynamicMethod, "v@:"); 
  13.     return YES; 
 
测试代码:
  1. RuntimeObj *obj = [[RuntimeObj alloc]init]; 
  2. [obj performSelector:@selector(dynamicMethod:)]; 
 
看看代码运行效果
 
讲解: 
* + (BOOL) resolveInstanceMethod:(SEL)aSEL 是在调用此类方法时,如果没有这个方法就会掉这个函数。
 
* class_addMethod 就是动态给类添加方法 原型 BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)  
 
 注:IMP 是函数指针
 
* “v@:” 是参数的一种写法 以后会做详细讲解
  
替换已有函数
在混合编码的时候不能按照已有思路执行原来的函数,那我们把它替换掉不就好了嘛,看Runtime是怎么做到的?
 
先上代码:(注讲下面的代码是为了讲targetReplaceMethod 替换成 demoReplaceMethod)
  1. void demoReplaceMethod(id SELF, SEL _cmd) 
  2.     NSLog(@"demoReplaceMethod"); 
  3.   
  4. -(void)replaceMethod 
  5.     Class strcls = [self class]; 
  6.     SEL  targetSelector = @selector(targetRelplacMethod); 
  7.     class_replaceMethod(strcls,targetSelector,(IMP)demoReplaceMethod,NULL); 
  8.   
  9. -(void)targetRelplacMethod 
  10.     NSLog(@"targetRelplacMethod"); 
 
测试代码:
  1. RuntimeObj *obj = [[RuntimeObj alloc]init]; 
  2. [obj replaceMethod]; 
  3. [obj targetRelplacMethod]; 
 
运行结果:
  1. 2014-05-12 19:38:37.490 Runtime[1497:303] demoReplaceMethod 
 
是不是原来的  NSLog(@”targetRelplacMethod”); 这句话就没有执行 被替换掉了!
 
注:
1. class_replaceMethod 方法就是动态替换Method的函数,原型 IMP 。
2. class_replaceMethod(Class cls, SEL name,IMP imp, const char *types) 返回值就是一个新函数的地址(IMP指针)。
3. 在实际项目中会经常用到这种方式, 比如:iOS 7以及7以下绘制NavigationBar, 自己慢慢体会吧。
 
动态挂载对象
挂载这个词语大家应该并不陌生吧,但是在这里有一点点微妙的不同,在这里博主也不是很好解释这个词语到底什么含义,那我来举个例子吧
 
如:如果你在对象传递(传参)的时候需要用到某个属性,按照以往的思路:我继承这个类重新一个新类就完事了,OK,这个思路没有问题,但是你不觉得要新建一个.h和一个.m文件有点麻烦?程序员都是懒惰的,要是有一个方法能直接讲我想要的属性挂载上前去岂不是更好?代码简单、易懂。看了标题你就应该知道Runtime能帮你实现你的愿望。
 
下面就来讲解下如何使用Runtime来 在已有对象上动态挂载另外一个对象。
 
先不说 直接放代码(这里以UIAlertView为例子):
  1. //挂载对象所需要的参数(UIAlertView挂载对象) 
  2. static const char kRepresentedObject; 
  3. -(void)showAlert:(id)sender 
  4.     UIAlertView *alert = [[UIAlertView alloc]initWithTitle:@"提示" message:message delegate:self cancelButtonTitle:@"取消" otherButtonTitles:@"去看看", nil]; 
  5.     alert.tag = ALERT_GOTO_TAG; 
  6.     objc_setAssociatedObject(alert, &kRepresentedObject, 
  7.     @"我是被挂载的", 
  8.     OBJC_ASSOCIATION_RETAIN_NONATOMIC); 
  9.     [alert show]; 
 
这个只是挂载看看如何去获取我们挂载的对象(NSString @“我是被挂载的”)
  1. -(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex 
  2.     if (buttonIndex == 1) { 
  3.         NSString *str = objc_getAssociatedObject(alertView, 
  4.                                                     &kRepresentedObject); 
  5.         NSLog(@"%@",str) 
  6.     }  
 
自己动手编写代码看看效果 是不是和你想的一样?
 
下面讲解下:
1. static const char kRepresentedObject; 这个只是一个标记,但是必不可少 具体什么作用没做过调研,我觉得应该就是你挂载的一个标记 Runtime 应该会根据这个标记来区别被挂载对象是挂载在哪个实例上。
2. objc_setAssociatedObject 动态设置关联对象(也就是挂载)。
3. objc_getAssociatedObject 动态获取关联对象 看到没有这里也要传 kRepresentedObject 这个标记,好像有点证明我前面的猜想了。
 
 
 

理解 Objective-C Runtime

2014-10-08 13:21 编辑: pockry 分类:iOS开发 来源:Cocoabit
19459

当人们初学 Cocoa/Objective-C 时,Objective-C Runtime 是被忽略的特性之一。原因是 Objective-C(这门语言)很容易在几小时内就熟悉,新学 Cocoa 的人花费他们大部分的时间学习 Cocoa 框架和适应它是如何工作的。然而每个人至少应该知道一些 runtime 的工作细节,需要比知道编译器会把 [target doMethodWith:var1];  转换为 objc_msgSend(target,@selector(doMethodWith:),var1); 更深入一些。知道 Objective-C 正在做的会让你更深入的理解 Objective-C 和你正在运行的 app。我认为 Mac/iPhone 的开发者不管你现在是什么水平,都会有收获的。

Objective-C Runtime 是开源的

Objective-C 是开源的,任何时候你都能从 http://opensource.apple.com. 获取。事实上查看 Objective-C 源码是我理解它是如何工作的第一种方式,在这个问题上要比读苹果的文档要好。你可以下载适合 Mac OS X 10.6.2 的 objc4-437.1.tar.gz。(译注:最新objc4-551.1.tar.gz

动态 vs 静态语言

Objective-C 是面相运行时的语言(runtime oriented language),就是说它会尽可能的把编译和链接时要执行的逻辑延迟到运行时。这就给了你很大的灵活性,你可以按需要把消息重定向给合适的对象,你甚 至可以交换方法的实现,等等(译注:在 Objective-C 中调用一个对象的方法可以看成向一个对象发送消息, Method Swizzling 具体实现可以参看 jrswizzle )。这就需要使用 runtime,runtime 可以做对象自省查看他们正在做的和不能做的(don't respond to)并且合适的分发消息(译注:感兴趣的同学可以查看 NSObject 类的 – forwardingTargetForSelector: 和 – forwardInvocation: 方法。P.S. 不是 NSObject 协议! )。如果我们和 C 这样的语言对比。在 C 里,你从 main() 方法开始写然后就是从上到下的写逻辑了并按你写代码的顺序执行程序。一个 C 的结构体不能转发函数执行请求到其他的目标上(other targets)。很可能你的程序是这样的:

1
2
3
4
5
6
#include 
int main(int argc, const char **argv[])
{        
    printf("Hello World!");
    return 0;
}

编译器解析,优化然后把优化后的代码转成汇编:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
.text
 .align 4,0x90
 .globl _main
_main:
Leh_func_begin1:
 pushq %rbp
Llabel1:
 movq %rsp, %rbp
Llabel2:
 subq $16, %rsp
Llabel3:
 movq %rsi, %rax
 movl %edi, %ecx 
 movl %ecx, -8(%rbp)
 movq %rax, -16(%rbp) 
 xorb %al, %al 
 leaq LC(%rip), %rcx 
 movq %rcx, %rdi 
 call _printf 
 movl $0, -4(%rbp) 
 movl -4(%rbp), %eax 
 addq $16, %rsp 
 popq %rbp 
 ret
Leh_func_end1: 
 .cstring
LC: 
 .asciz "Hello World!"

然后链接库并生成可执行程序(译注:如果你对 C 的编译链接过程还不熟悉可以参看 Deep C and C++)。要和 Objective-C 对比的话,处理过程很相似,生成的代码依赖于是否有 Objective-C Runtime 库。当刚学 Objective-C 时,我们最先了解的(最简单的那种)是 Objective-C 中用括号包起来的代码像这样…

1
[self doSomethingWithVar:var1];

被转换为…

1
objc_msgSend(self,@selector(doSomethingWithVar:),var1);

但除了这些,我们就不知道之后在运行时做了什么了。

Objective-C Runtime 是什么?

Objective-C 的 Runtime 是一个运行时库(Runtime Library),它是一个主要使用 C 和汇编写的库,为 C 添加了面相对象的能力并创造了 Objective-C。这就是说它在类信息(Class information) 中被加载,完成所有的方法分发,方法转发,等等。Objective-C runtime 创建了所有需要的结构体,让 Objective-C 的面相对象编程变为可能。

Objective-C Runtime 术语

更深入之前,咱们先了解点术语。Mac 和 iPhone 开发者关心的有两个 runtime:Modern Runtime(现代的 Runtime) 和 Legacy Runtime(过时的 Runtime)。Modern Runtime:覆盖所有 64 位的 Mac OS X 应用和所有 iPhone OS 的应用。 Legacy Runtime: 覆盖其他的所有应用(所有 32 位的 Mac OS X 应用) Method 有 2 种基本类型的方法。Instance Method(实例方法):以 ‘-’ 开始,比如 -(void)doFoo; 在对象实例上操作。Class Method(类方法):以 ‘+’ 开始,比如 +(id)alloc。方法(Methods)和 C 的函数很像,是一组代码,执行一个小的任务,如:

1
2
3
4
- (NSString *)movieTitle
{
    return @"Futurama: Into the Wild Green Yonder";
}

Selector 在 Objective-C 中 selector 只是一个 C 的数据结构,用于表示一个你想在一个对象上执行的 Objective-C 方法。在 runtime 中的定义像这样…

1
typedef struct objc_selector  *SEL;

像这样使用…

1
SEL aSel = @selector(movieTitle);

Message(消息)

1
[target getMovieTitleForObject:obj];

消息是方括号 ‘[]’ 中的那部分,由你要向其发送消息的对象(target),你想要在上面执行的方法(method)还有你发送的参数(arguments)组成。 Objective-C 的消息和 C 函数调用是不同的。事实上,你向一个对象发送消息并不意味着它会执行它。Object(对象)会检查消息的发送者,基于这点再决定是执行一个不同的方法还是转发消息到另一个目标对象上。Class 如果你查看一个类的runtime信息,你会看到这个…

1
2
3
4
typedef struct objc_class *Class;
typedef struct objc_object {
    Class isa;
} *id;

这里有几个事情。我们有一个 Objective-C 类的结构体和一个对象的结构体。objc_object 只有一个指向类的 isa 指针,就是我们说的术语 “isa pointer”(isa 指针)。这个 isa 指针是当你向对象发送消息时,Objective-C Runtime 检查一个对象并且查看它的类是什么然后开始查看它是否响应这些 selectors 所需要的一切。最后我么看到了 id 指针。默认情况下 id 指针除了告诉我们它们是 Objective-C 对象外没有其他用了。当你有一个 id 指针,然后你就可以问这个对象是什么类的,看看它是否响应一个方法,等等,然后你就可以在知道这个指针指向的是什么对象后执行更多的操作了。你可以在 LLVM/Clang 的文档中的 Block 中看到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct Block_literal_1 {
    void *isa; // initialized to &_NSConcreteStackBlock or &_NSConcreteGlobalBlock    
    int flags;    
    int reserved;     
    void (*invoke)(void *, ...); 
    struct Block_descriptor_1 { 
        unsigned long int reserved; // NULL     
        unsigned long int size;  // sizeof(struct Block_literal_1)
        // optional helper functions     
        void (*copy_helper)(void *dst, void *src);
        void (*dispose_helper)(void *src);     
    } *descriptor;    
    // imported variables
};

Blocks 被设计为兼容 Objective-C 的 runtime,所以他们被作为对象对待,因此他们可以响应消息,比如 -retain,-release,-copy ,等等。IMP(方法实现 Method Implementations)

1
typedef id (*IMP)(id self,SEL _cmd,...);

IMP 是指向方法实现的函数指针,由编译器为你生成。如果你新接触 Objective-C 你现在不需要直接接触这些,但是我们将会看到,Objective-C  runtime 将如何调用你的方法的。Objective-C Classes(Objective-C 类) 那么什么是 Objective-C 类?在 Objective-C 中的一个类实现看起来像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@interface MyClass : NSObject {
    // vars
    NSInteger counter;
}
// methods
-(void)doFoo;
@end
 
但是 runtime 不只要追踪这些
 
#if !__OBJC2__   
    Class super_class                        OBJC2_UNAVAILABLE;
    const char *name                         OBJC2_UNAVAILABLE;
    long version                             OBJC2_UNAVAILABLE;
    long info                                OBJC2_UNAVAILABLE;
    long instance_size                       OBJC2_UNAVAILABLE;    
    struct objc_ivar_list *ivars             OBJC2_UNAVAILABLE;    
    struct objc_method_list **methodLists    OBJC2_UNAVAILABLE;    
    struct objc_cache *cache                 OBJC2_UNAVAILABLE;    
    struct objc_protocol_*protocols          OBJC2_UNAVAILABLE;
#endif

我们可以看到,一个类有其父类的引用,它的名字,实例变量,方法,缓存还有它遵循的协议。runtime 在响应类或实例的方法时需要这些信息。

那么 Class 定义的是对象还是对象本身?它是如何实现的 (译注:读者需要区分 Class 和 class 是不同的,正如 Nil 和 nil 的用途是不同的)

是的,之前我说过 Objective-C 类也是对象,runtime 通过创建 Meta Classes 来处理这些。当你发送一个消息像这样 [NSObject alloc] 你正在向类对象发送一个消息,这个类对象需要是 MetaClass 的实例,MetaClass 也是 root meta class 的实例。当你说继承自 NSObject 时,你的类指向 NSObject 作为自己的 superclass。然而,所有的 meta class 指向 root metaclass 作为自己的 superclass。所有的 meta class 只是简单的有一个自己响应的方法列表。所以当你向一个类对象发送消息如 [NSObject alloc],然后实际上 objc_msgSend() 会检查 meta class 看看它是否响应这个方法,如果他找到了一个方法,就在这个 Class 对象上执行(译注:class 是一个实例对象的类型,Class 是一个类(class)的类型。对于完全的 OO 来说,类也是个对象,类是类类型(MetaClass)的实例,所以类的类型描述就是 meta class)。

为什么我们继承自苹果的类

从你开始 Cocoa 开发时,那些教程就说如继承自 NSObject 然后开始写一些代码,你享受了很多继承自苹果的类所带来的便利。有一件事你从未意识到的是你的对象被设置为使用 Objective-C 的 runtime。当我们为我们的类的一个实例分配了内存,像这样…

1
MyObject *object = [[MyObject alloc] init];

最先执行的消息是 +alloc。如果你查看下文档, 它说“新的实例对象的 isa 实例变量被初始化为指向一个数据结构,那个数据结构描述了这个类;其他的实例变量被初始化为 0。”所以继承自苹果的类不仅仅是继承了一些重要的属性,也继承了能在内存中轻松分配内存的能力和在内存中创建满足 runtime 期望的对象结构(设置 isa 指针指向我们的类)。

那么 Class Cache 是什么?(objc_cache *cache)

当 Objective-C runtime 沿着一个对象的 isa 指针检查时,它会发现一个对象实现了许多的方法。然而你可能只调用其中一小部分的方法,也没有意义每次检查时搜索这个类的分发表(dispatch table)中的所有 selector。所以这个类实现了一个缓存,当你搜索一个类的分发表,并找到合适的 selector 后,就会把它放进缓存中。所以当 objc_msgSend() 在一个类中查找 selector 时会先查找类缓存。有个理论是,当你在一个类上调用了一个消息,你很可能之后还会调用它。所以如果我们考虑到这点,就意味着当我们有个子类继承自 NSObject 叫做 MyObject 并且运行了以下的代码

1
2
3
4
5
6
7
8
9
10
MyObject *obj = [[MyObject alloc] init]; 
 
@implementation MyObject
- (id)init {
    if(self = [super init]) {
        [self setVarA:@”blah”];    
    }
    return self;
}
@end

发生了以下的事:

(1) [MyObject alloc] 首先被执行。MyObject 没有实现 alloc 方法,所以我们不能在这个类中找到 +alloc 方法,然后沿着 superclass 指针会指向 NSObject。

(2) 我们询问 NSObject 是否响应 +alloc 方法,它可以。+alloc 检查消息的接收者类,是 MyObject,然后分配一块和我们的类同样大小的内存空间,并初始化它的 isa 指针指向 MyObject 类,我们现在有了一个实例对象,最终把类对象的 +alloc 方法加入 NSObject 的类缓存(class cache)中(lastly we put +alloc in NSObject's class cache for the class object )。

(3) 到现在为止,我们发送了一个类消息,但是现在我们发送一个实例消息,只是简单的调用 -init 或者我们设计的初始化方法。当然,我们的类会响应这个方法,所以 -(id)init 加入到缓存中。(译注:要是 MyObject 实现了 init 方法,就会把 init 方法加入到 MyObject 的 class cache 中,要是没有实现,只是因为继承才有了这个方法,init 方法还是会加入到 NSObject 的 class cache 中)。

(4) 然后 self = [super init] 被调用。super 是个 magic keyword,指向对象的父类,所以我们得到了 NSObject 并调用它的的 init 方法。这样可以确保 OOP(面相对象编程) 的继承功能正常,这个方法可以正确的初始化父类的变量,之后你(在子类中)可以初始化自己的变量,如果需要可以覆盖父类的方法。在 NSObject 的例子中,没什么重要的要做,但并不总是这样。有时要做些重要的初始化。比如…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#import  
 
@interface MyObject : NSObject {
    NSString *aString;
    @property(retain) NSString *aString; 
 
@end 
 
@implementation MyObject
 
-(id)init 
    if (self = [super init]) {
        [self setAString:nil];
    }
    return self;
}
 
@synthesize aString;
 
@end
 
int main (int argc, const char * argv[]) 
{
    NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
 
    id obj1 = [NSMutableArray alloc]; 
    id obj2 = [[NSMutableArray alloc] init];   
    id obj3 = [NSArray alloc]; 
    id obj4 = [[NSArray alloc] initWithObjects:@"Hello",nil];
 
    NSLog(@"obj1 class is %@",NSStringFromClass([obj1 class]));
    NSLog(@"obj2 class is %@",NSStringFromClass([obj2 class]));
    NSLog(@"obj3 class is %@",NSStringFromClass([obj3 class]));
    NSLog(@"obj4 class is %@",NSStringFromClass([obj4 class]));
 
    id obj5 = [MyObject alloc]; 
    id obj6 = [[MyObject alloc] init];   
 
    NSLog(@"obj5 class is %@",NSStringFromClass([obj5 class]));    
    NSLog(@"obj6 class is %@",NSStringFromClass([obj6 class]));   
 
    [pool drain];
    return 0;
}

现在如果你新接触 Cocoa ,我让你猜会会输出什么,你可能会说

1
2
3
4
5
6
NSMutableArray
NSMutableArray 
NSArray
NSArray
MyObject
MyObject

但是,实际上是

1
2
3
4
5
6
obj1 class is __NSPlaceholderArray
obj2 class is NSCFArray
obj3 class is __NSPlaceholderArray
obj4 class is NSCFArray
obj5 class is MyObject
obj6 class is MyObject

这是因为在 Objective-C 中 +alloc 方法可能会返回某个类的对象,然后在 -init 中返回另一个类的对象。
(译注:感兴趣的同学可以看下这两篇文章:Class ClustersMake Your Own Abstract Factory Class Cluster in Objective-C, 第二篇文章需要自备小梯子。)

那么在 objc_msgSend 中发生了什么?

事实上在 objc_msgSend() 中发生了许多事儿。假设我们有这样的代码…

1
[self printMessageWithString:@"Hello World!"];

它实际上会被编译器翻译为…

1
objc_msgSend(self,@selector(printMessageWithString:),@"Hello World!");

我们沿着目标对象的 isa 指针查找,看看是否这个对象响应 @selector(printMessageWithString:) selector。假设我们在类的分发表或者缓存中找到了这个 selector,我们沿着函数指针并且执行它。这样 objcmsgSend() 就永远不会返回,它开始执行,然后沿着指向方法的指针,然后你的方法返回,这样看起来 objcmsgSend() 方法返回了。Bill Bumgarner 比我讲了更多 objc_msgSend() 的细节(部分1部分2 和 部分3)。

概括下他说的,并且你已经看过了 Objective-C 的 runtime 代码…

检查忽略的 Selector 和短路(Short Circut)—— 显然,如果我们运行在垃圾回收机制下,我们可以忽略调用 -retain, -release, 等等。

检查 nil 对象(target)。和其他的语言不一样的是,在 Objective-C 中向 nil 发送消息是完全合法的,并且有些原因下你会愿意这么做的。假设我们有个非 nil 的对象,然后我们继续…

然后我们需要在这个类上找到 IMP,所以我们先从 class cache 中找起,如果找到了就沿着指针跳到这个函数。

如果没有在缓存中找到 IMP,然后去查找类的分发表,如果找到了,就沿着指针跳到这个函数。

如果 IMP 没有在缓存和类的分发表中找到,然后我们跳到转发机制。这意味着最终你的代码被编译器转换为 C 函数。你写的方法会像这样…

1
-(int)doComputeWithNum:(int)aNum

会被翻译为…

1
int aClass_doComputeWithNum(aClass *self,SEL _cmd,int aNum)

Objective-C Runtime 通过调用(invoking)指向这些方法的函数指针调用你的方法(call your methods)。现在,我要说的是,你不能直接调用这些被翻译的方法,但是 Cocoa 框架提供了获得函数指针的方法…

1
2
3
4
5
6
7
8
9
//declare C function pointer
int (computeNum *)(id,SEL,int);
 
//methodForSelector is COCOA & not ObjC Runtime
//gets the same function pointer objc_msgSend gets
computeNum = (int (*)(id,SEL,int))[target methodForSelector:@selector(    doComputeWithNum:)]; 
 
//execute the C function pointer returned by the runtime
computeNum(obj,@selector(doComputeWithNum:),aNum);

通过这种方法,你可以直接访问这个函数,并且可以在运行时直接调用,甚至可以使用这个避开 runtime 的动态特性,如果你绝对需要确保一个方法被执行。Objective-C 就是用这种途径去调用你的方法的,但是使用的是 objc_msgSend()。

Objective-C 消息转发

在 Objective-C 中向一个不知道如何响应这个方法的对象发送消息是完全合法的(甚至可能是一种潜在的设计决定)。苹果的文档中给出的一个原因是模拟多继 承,Objective-C 不是原生支持的,或者你可能只是想抽象你的设计并且隐藏幕后处理这些消息的其他对象/类。这一点是 runtime 非常需要的。它是这样做的 1. Runtime 检查了你的类和所有父类的 class cache 和分发表,但是没找到指定的方法。2. Objective_C 的 Runtime  会在你的类上调用 + (BOOL) resolveInstanceMethod:(SEL)aSEL。 这就给了你一个机会去提供一个方法实现并且告诉 runtime 你已经解析了这个方法,如果它开始查找,这回就会找到这个方法。你可以像这样实现…定义一个函数…

1
2
3
4
void fooMethod(id obj, SEL _cmd)
    NSLog(@"Doing Foo");
}

然后你可以像这样使用 class_addMethod() 解析它…

1
2
3
4
5
6
7
8
9
+(BOOL)resolveInstanceMethod:(SEL)aSEL
{
    if(aSEL == @selector(doFoo:))
    {
            class_addMethod([self class],aSEL,(IMP)fooMethod,"v@:");
            return YES;
    }
    return [super resolveInstanceMethod];
}

在 class_addMethod() 最后一部分的 "v@:" 是方法的返回和参数类型。你可以在 Runtime Guide 的 Type Encoding 章节看到完整介绍。 3. Runtime 然后调用 – (id)forwardingTargetForSelector:(SEL)aSelector。这样做是为了给你一次机会(因为我们不能解析这个方法 (参见上面的 #2))引导 Objective-C runtime 到另一个可以响应这个消息的对象上,在花费昂贵的处理过程调用  – (void)forwardInvocation:(NSInvocation *)anInvocation 之前调用这个方法也是更好的。你可以像这样实现

1
2
3
4
5
6
7
8
- (id)forwardingTargetForSelector:(SEL)aSelector
{
    if(aSelector == @selector(mysteriousMethod:))
    {        
        return alternateObject;
    }
    return [super forwardingTargetForSelector:aSelector];
}

显然你不想从这个方法直接返回 self,否则可能会产生一个死循环。 4. Runtime 最后一次会尝试在目标对象上调用 – (void)forwardInvocation:(NSInvocation *)anInvocation。如果你从没看过 NSInvocation,它是 Objective-C 消息的对象形式。一旦你有了一个 NSInvocation 你可以改变这个消息的一切,包括目标对象,selector 和参数。所以你可以这样做…  

1
2
3
4
5
6
7
8
9
-(void)forwardInvocation:(NSInvocation *)invocation
{  
    SEL invSEL = invocation.selector;    
    if([altObject respondsToSelector:invSEL]) {        
        [invocation invokeWithTarget:altObject];    
    else {        
        [self doesNotRecognizeSelector:invSEL];    
    }
}

如果你继承自 NSObject,默认它的 – (void)forwardInvocation:(NSInvocation *)anInvocation 实现只是简单的调用 -doesNotRecognizeSelector:,你可以在最后一次机会里覆盖这个方法去做一些事情。(译注:对这块内容有兴趣的同学可以参见:http://www.cnblogs.com/biosli/p/NSObjectinherit2.html

Non Fragile ivars(Modern Runtime)(非脆弱的 ivar)

我们最近在 Modern Runtime 里得到的是 Non Fragile ivars 的概念。当编译你的类时,编译器生成了一个 ivar 布局,显示了在你的类中从哪可以访问你的 ivars,获取指向你的对象的指针,查看 ivar 与对象起始字节的偏移关系,和获取读入的变量类型的总共字节大小等一些底层的细节。所以你的 ivar 布局可能看起来像这样,左侧的数字是字节偏移量。

2014031602.png

我们有了 NSObject 的 ivar 布局,然后我们继承自 NSObject 去扩展它并且添加了我们自己的 ivars。在苹果发布更新前这都工作的很好,但是 Mac OS X 10.6 发布后,就成了这样

2014031603.png

你的自定义对象被剔除了因为我们有了一个重叠的父类。唯一可以防止这个的办法是如果苹果坚持之前的布局,如果他们这么做了,那么他们的框架就不能改进,因 为他们的 ivar 布局被冻住了。在 fragile ivar 下你不得不重新编译你继承自苹果类的类来恢复兼容性。所以在非 fragile ivar 时,会发生生么?

2014031604.png

使用非 fragile ivars 时,编译器生成和 fragile ivars 相同的 ivar 布局。然而当 runtime 检测到一个重叠的超类时,它调整你在这个类中新增的 ivar 的偏移量,这样在子类中新增加的那部分就显示出来了。

Objective-C 关联对象

最近在 Mac OS X 10.6 雪豹 中新引入了关联引用。Objective-C 不能动态的添加一些属性到对象上,和其他的一些原生支持这点的语言不一样。所以之前你都不得不努力为未来要增加的变量预留好空间。在 Mac OS X 10.6 中,Objective-C 的 Runtime 已经原生的支持这个功能了。如果我们想向一个已有的类添加变量,看起来像这样…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#import  //Cocoa
#include  //objc runtime api’s 
 
@interface NSView (CustomAdditions)
@property(retain) NSImage *customImage;
@end 
 
@implementation NSView (CustomAdditions) 
 
static char img_key; //has a unique address (identifier)
 
- (NSImage *)customImage
{    
    return objc_getAssociatedObject(self,&img_key);
}
 
- (void)setCustomImage:(NSImage *)image
{    
    objc_setAssociatedObject(self, &img_key,image, OBJC_ASSOCIATION_RETAIN);
 
@end
 
objc_setAssociatedObject() 的选项,你可以在 runtime.h 文件中找到。
 
/* Associated Object support. */ 
 
/* objc_setAssociatedObject() options */
enum {    
    OBJC_ASSOCIATION_ASSIGN = 0,    
    OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1,    
    OBJC_ASSOCIATION_COPY_NONATOMIC = 3,    
    OBJC_ASSOCIATION_RETAIN = 01401,    
    OBJC_ASSOCIATION_COPY = 01403
};

这些和 @property 语法中的选项意思一样。

混和的 vTable Dispatch

如果你看过 modern runtime 的代码,你会发现这个(在 objc-runtime-new.m 中)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
/***********************************************************************
* vtable dispatch
* Every class gets a vtable pointer. The vtable is an array of IMPs.
* The selectors represented in the vtable are the same for all classes
*   (i.e. no class has a bigger or smaller vtable).
* Each vtable index has an associated trampoline which dispatches to 
*   the IMP at that index for the receiver class's vtable (after 
*   checking for NULL). Dispatch fixup uses these trampolines instead 
*   of objc_msgSend.
* Fragility: The vtable size and list of selectors is chosen at launch 
*   time. No compiler-generated code depends on any particular vtable 
*   configuration, or even the use of vtable dispatch at all.
* Memory size: If a class's vtable is identical to its superclass's 
*   (i.e. the class overrides none of the vtable selectors), then 
*   the class points directly to its superclass's vtable. This means 
*   selectors to be included in the vtable should be chosen so they are 
*   (1) frequently called, but (2) not too frequently overridden. In 
*   particular, -dealloc is a bad choice.
* Forwarding: If a class doesn't implement some vtable selector, that 
*   selector's IMP is set to objc_msgSend in that class's vtable.
* +initialize: Each class keeps the default vtable (which always 
*   redirects to objc_msgSend) until its +initialize is completed.
*   Otherwise, the first message to a class could be a vtable dispatch, 
*   and the vtable trampoline doesn't include +initialize checking.
* Changes: Categories, addMethod, and setImplementation all force vtable 
*   reconstruction for the class and all of its subclasses, if the 
*   vtable selectors are affected.
**********************************************************************/

背后的思想是,runtime 尝试在这个 vtable 中存储最近被调用的 selectors,这样就可以提升你的应用的速度,因为它使用了比 objc_msgSend 更少的指令(fewer instructions)。vtable 中保存 16 个全局最经常调用的 selectors,事实上顺着代码往下看你可以发现垃圾回收和非垃圾回收类型程序的默认 selectors :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
static const char * const defaultVtable[] = {
    "allocWithZone:"
    "alloc"
    "class"
    "self"
    "isKindOfClass:"
    "respondsToSelector:"
    "isFlipped"
    "length"
    "objectForKey:"
    "count"
    "objectAtIndex:"
    "isEqualToString:"
    "isEqual:"
    "retain"
    "release"
    "autorelease"
};
static const char * const defaultVtableGC[] = {
    "allocWithZone:"
    "alloc"
    "class"
    "self"
    "isKindOfClass:"
    "respondsToSelector:"
    "isFlipped"
    "length"
    "objectForKey:"
    "count"
    "objectAtIndex:"
    "isEqualToString:"
    "isEqual:"
    "hash"
    "addObject:"
    "countByEnumeratingWithState:objects:count:"
};

你可以在调试时从堆栈追踪里找到其中的method,可以像objc_msgSend()一样将它们用于调试。

总结

Objective-C Runtime是非常优秀的作品,它为支撑我们的Cocoa/Objective-C app以及众多的优秀特性做了大量工作。你可以查看苹果官方文档来继续深入了解(Objective-C Runtime Programming GuideObjective-C Runtime Reference)。

 
 
 
 
 
 

Objective-C总Runtime的那点事儿(一)消息机制

2014-10-18 16:28 编辑: pockry 分类:iOS开发 来源:小笨狼的专栏
40789

最近在找工作,Objective-C中的Runtime是经常被问到的一个问题,几乎是面试大公司必问的一个问题。当然还有一些其他问题也几乎必问,例 如:RunLoop,Block,内存管理等。其他的问题如果有机会我会在其他文章中介绍。本篇文章主要介绍RunTime。

RunTime简称运行时。就是系统在运行的时候的一些机制,其中最主要的是消息机制。对于C语言,函数的调用在编译的时候会决定调用哪个函数( C语言的函数调用请看这里 )。编译完成之后直接顺序执行,无任何二义性。OC的函数调用成为消息发送。属于动态调用过程。在编译的时候并不能决定真正调用哪个函数(事实证明,在编 译阶段,OC可以调用任何函数,即使这个函数并未实现,只要申明过就不会报错。而C语言在编译阶段就会报错)。只有在真正运行的时候才会根据函数的名称找 到对应的函数来调用。

那OC是怎么实现动态调用的呢?下面我们来看看OC通过发送消息来达到动态调用的秘密。假如在OC中写了这样的一个代码:

1
[obj makeText];

其中obj是一个对象,makeText是一个函数名称。对于这样一个简单的调用。在编译时RunTime会将上述代码转化成

1
objc_msgSend(obj,@selector(makeText));

首先我们来看看obj这个对象,iOS中的obj都继承于NSObject。

1
2
3
@interface NSObject <nsobject> {
    Class isa  OBJC_ISA_AVAILABILITY;
}</nsobject>

在NSObjcet中存在一个Class的isa指针。然后我们看看Class:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef struct objc_class *Class;
struct objc_class {
  Class isa; // 指向metaclass
   
  Class super_class ; // 指向其父类
  const char *name ; // 类名
  long version ; // 类的版本信息,初始化默认为0,可以通过runtime函数class_setVersion和class_getVersion进行修改、读取
  long info; // 一些标识信息,如CLS_CLASS (0x1L) 表示该类为普通 class ,其中包含对象方法和成员变量;CLS_META (0x2L) 表示该类为 metaclass,其中包含类方法;
  long instance_size ; // 该类的实例变量大小(包括从父类继承下来的实例变量);
  struct objc_ivar_list *ivars; // 用于存储每个成员变量的地址
  struct objc_method_list **methodLists ; // 与 info 的一些标志位有关,如CLS_CLASS (0x1L),则存储对象方法,如CLS_META (0x2L),则存储类方法;
  struct objc_cache *cache; // 指向最近使用的方法的指针,用于提升效率;
  struct objc_protocol_list *protocols; // 存储该类遵守的协议
    }

我们可以看到,对于一个Class类中,存在很多东西,下面我来一一解释一下:

Class isa:指向metaclass,也就是静态的Class。一般一个Obj对象中的isa会指向普通的Class,这个Class中存储普通成员变量和对 象方法(“-”开头的方法),普通Class中的isa指针指向静态Class,静态Class中存储static类型成员变量和类方法(“+”开头的方 法)。

Class super_class:指向父类,如果这个类是根类,则为NULL。

下面一张图片很好的描述了类和对象的继承关系:

iuqQFnm.png

注意:所有metaclass中isa指针都指向跟metaclass。而跟metaclass则指向自身。Root metaclass是通过继承Root class产生的。与root class结构体成员一致,也就是前面提到的结构。不同的是Root metaclass的isa指针指向自身。

Class类中其他的成员这里就先不做过多解释了,下面我们来看看:

@selector (makeText):这是一个SEL方法选择器。SEL其主要作用是快速的通过方法名字(makeText)查找到对应方法的函数指针,然后调用其函 数。SEL其本身是一个Int类型的一个地址,地址中存放着方法的名字。对于一个类中。每一个方法对应着一个SEL。所以iOS类中不能存在2个名称相同 的方法,即使参数类型不同,因为SEL是根据方法名字生成的,相同的方法名称只能对应一个SEL。

下面我们就来看看具体消息发送之后是怎么来动态查找对应的方法的。

首先,编译器将代码[obj makeText];转化为objc_msgSend(obj, @selector (makeText));,在objc_msgSend函数中。首先通过obj的isa指针找到obj对应的class。在Class中先去cache中 通过SEL查找对应函数method(猜测cache中method列表是以SEL为key通过hash表来存储的,这样能提高函数查找速度),若 cache中未找到。再去methodList中查找,若methodlist中未找到,则取superClass中查找。若能找到,则将method加 入到cache中,以方便下次查找,并通过method中的函数指针跳转到对应的函数中去执行。

详解Objective-C runtime

发布于:2014-01-06 11:28阅读数:12617

本文是我在 Alt Tech Talks: London 上关于 Objective-C runtime的演讲总结,如果你对Objective-C runtime感兴趣的话,应该看看这篇文章,特别是文章中的代码链接,一定会受益匪浅。 

 
阅读器

原文地址:http://blog.securemacprogramming.com/2013/12/by-your-_cmd/

 
感谢翻译小组成员wingpan热心翻译。本篇文章是我们每周推荐优秀国外的技术类文章的其中一篇。如果您有不错的原创或译文,欢迎提交给我们,更欢迎其他朋友加入我们的翻译小组(联系qq:2408167315)。
 
本文是我在 Alt Tech Talks: London 上关于 Objective-C runtime的演讲总结,如果你对Objective-C runtime感兴趣的话,应该看看这篇文章,特别是文章中的链接,一定会受益匪浅。 
 
什么是Objective-C runtime?
简单来说,Objective-C runtime是一个实现Objective-C语言的C库。对象可以用C语言中的结构体表示,而方法(methods)可以用C函数实现。事实上,他们差不多也是这么干了,另外再加上了一些额外的特性。这些结构体和函数被runtime函数封装后,Objective-C程序员可以在程序运行时创建,检查,修改类,对象和它们的方法。
 
 除了封装,Objective-C runtime库也负责找出方法的最终执行代码。当程序执行[object doSomething]时,不会直接找到方法并调用。相反,一条消息(message)会发送给对象(在这儿,我们通常叫它接收者)。runtime库给次机会让对象根据消息决定该作出什么样的反应。Alan Kay反复强调消息传递(message-passing)是Smalltalk最重要的部分(Objective-C根据Smalltalk发展而来),而不是对象: 
 
由于以前关于这个话题我创造了“对象”这个词,现在很多人都对这个概念趋之若鹜,这让我感到非常遗憾。
 
其实这里面更为重要的理念是“消息命令”(messaging),这才是Smalltalk的核心内容(现在尚有一些内容还没有全部完成)。日语中有个简短的单词叫做“ma”,它用来表示两个物体之间的东西,在英语中和它最相近的单词也许是“interstitial”。制造一个庞大且可扩展系统的关键是设计它各个模块之间的通信方式,而不是关注它的内部属性和行为。 
 
 
实际上,在一篇介绍Smalltalk虚拟机的文章里,这门编程技术被叫做消息传递或者消息传送范式。“面向对象”通常用来描述内存管理系统。
 
在演讲和文章中都使用ObjC runtime这个词,看似只有一个,实际上存在很多runtime库。虽然它们都支持对象的自省检查和消息接收,但是它们却有不同的特性和实现方式(例如,同样是发送消息,Apple的runtime用一步完成,而GNU runtime会先查询这些消息,然后执行查找到的函数分两步完成)。以下所有的讨论,都是基于Apple的最新runtime库(苹果公司在OSX 10.5和iOS发布时的版本)。 
 
在那次演讲中,我决定研究runtime库某些领域的功能。我找了一些希望更透彻了解的东西,然后把它们做成问答的形式组成我的演讲。
 
动态创建类
 
如何实现Key-Value Observing?
 当我在准备这次演讲时,一篇叫做KVO considered harmful 的文章开始拥有很多拥趸。它提出了很多对KVO正确的批评,但相对于舍弃观察者模式不用,我更想探索出一种新的实现方式。 
 
KVO实现观察者模式的关键是它偷偷摸摸将被观察对象的类改变了,它子类化原来的类后,就能够自定义该对象的方法来调用KVO的回调方法。这些都是通过 objc_duplicateClass这个方法完成,但很遗憾,这个方法并不公开,我们无法私自调用。 
 
条条大路通罗马,好在除了objc_duplicateClass,还有其他方法可以通过使用秘密子类化的方式实现观察者模式,比如创建和注册“class pair”。那么什么是class pair呢?对于Objective-C的类来说,都有一对Class的对象来定义它:Class对象定义了这个类的实例方法,而metaclass定义了这个类的类方法。所以每个class其实是它metaclass的单例。 
 
这个代码展示了观察者模式的工作原理。当你给对象增加观察者时,这个对象首先会检查自己是否可被观察,如果是,它会新创建一个类,用我们自己的-dealloc替代原来类的方法,同样它也会把-class方法替换掉,类似于KVO被观察对象,当你访问被观察对象的类名时,返回的是它原来的类名,而不是新生成的类。
 
创建完类后,我们需要照着 Key-Value Coding为属性增加一个setter方法:这个setter方法会获取这个属性修改前的值和修改后的值,然后调用block形式的回调函数,将这两个值告诉观察者。代码中根据我们的意愿,这个block可以异步调用。 
 
请注意, -addObserverForKey:withBlock:会使用s object_setClass() 将被观察对象的类替代为新组建的类。这样做最主要的目的是将消息转变为方法的方式改变,但是这需要非常小心,原来的类和新的类必须有相同的成员变量布局。因为成员变量也是用过runtime访问,修改某个对象的类可能导致runtime无法找到对应的变量。 
 
我们在存储观察者集合时遇到些麻烦,因为没地方去存它们。给ObserverPattern这个类增加成员变量不起作用,因为根本没有生成这个类的对象。被观察对象的成员变量是它原来类的,它并没有考虑过这些观察者。 
 
Objective-C runtime通过引入 associated objects 帮助我们摆脱这个困境。在runtime里,理论上所有对象都可以拥有包含其他对象的字典。通过associated references,被观察对象可以存储和访问他们的观察者,而不需要额外的成员变量。
 
如果你运行多次后,你会发现ObserverPattern 还是有点小毛病的。由于观察者回调是异步调用的,观察者接
 
收到的变化事件也是乱序的。这意味着观察者其实无法区分被观察属性的最终状态是什么,回调中的新值可能早已被修改。我这样做的目的是为了说明在KVO中同步调用回调其实是个有用的特色,并非bug。 
 
创建对象
 
那些额外的字节都是干啥用的?
当你创建一个 Objective-C对象时,runtime会在实例变量存储区域后面再分配一点额外的空间。这么做的目的是什么呢?你可以获取这块空间起始指针(用 object_getIndexedIvars),然后就可以索引实例变量(ivars)。好吧,下面我会使用自定义数组来说明一下索引ivars的用处。 
 
让我们创建一个数组!从这个SimpleArray中可以看到两件事情:最明显的一件是它使用了类簇模式。当使用+alloc方法返回对象时,一般情况下已经为这个对象分配了所有的内存,但是在这个例子中,在+alloc时并不知道需要多大的内存空间。只有当调用了 -initWithObjects:count:以后,才能根据数组内对象数量计算出这个数组需要多大的内存,所以+alloc只是返回一个占位符,只有在初始化后才会分配和返回真正的数组对象。 
 
或许你会问为什么我们要用类簇把事情搞那么复杂,使用 calloc()另外分配一块大小合适的缓存,然后把那些对象指针存到里面不就得了?答案是希望利用局部性原理提高访问性能。从数组的设计上我们可以看出,每次数组指针被访问时,之后会有很大几率访问到缓存指针,所以把它们肩并肩的放入内存意味着找到其中一个就是找到了另外一个。 
 
消息派发
 
消息如何转发?
Objective-C其中一个强大特性是对象不需要实现某个方法,尽管它在编译时声明了该选择符(selector)。但它可以在运行时再决定方法实现,或者将这些消息转发给其他对象,或者发出异常,亦或做一些其他事情。但是这个特性的某些方面曾经一直困扰我:消息转发(message forwarding)会调用 -forwardInvocation:,然后传入一个NSInvocation 对象。但是这个NSInvocation 类是在Foundation库中定义的,难道说runtime工作需要Foundation配合? 
 
我试着挖掘其中的原因,发现答案并不是我想的那样,runtime不需要知道Foundation。runtime会让程序定义转发函数(forwarding function),当 objc_msgSend()无法找到该selector的实现时,那个转发函数就会被调用。程序一启动,CoreFoundation就将 -forwardInvocation:定义成转发函数。 
 
让我们来创建一个Ruby!当然并不是真的实现完整的Ruby,Ruby有一个叫做#method_missing的函数,当对象收到一个它没有实现的消息时,这个函数就会被调到,这和Smalltalk的做法比较相似。使用objc_setForwardHandler,我们也能在Objective-C的类中实现类似Ruby的methodMissing:方法。 
 
总结
Objective-C runtime可以有效的帮助我们为程序增加很多动态的行为。一些开发者除了使用method swizzling帮助调试程序,并不会在实际程序中使用它,但runtime编程的确有很多功能,它应该成为实际应用代码编写的重要工具。
原文地址:https://www.cnblogs.com/fengmin/p/5015900.html