前言
Objective-C is the primary programming language you use when writing software for OS X and iOS. It"s a superset of the C programming language and provides object-oriented capabilities and a dynamic runtime. Objective-C inherits the syntax, primitive types, and flow control statements of C and adds syntax for defining classes and methods. It also adds language-level support for object graph management and object literals while providing dynamic typing and binding, deferring many responsibilities until runtime.
Objective-C(下面简称OC)是由C语言和Smalltalk扩展出来的,是C语言的超集,最大的区别是OC是面向对象的,其火星文写法对于之前从事Java开发的同学颇感蛋疼,OC最大特点之一是使用“消息结构”而不是“函数调用”,所以在运行时执行的代码由运行环境决定,而Java是由编译器决定。个人感觉有关于IOS学习的文章相较于Android质量较低,有可能是苹果系统封闭的原因,本文侧重介绍常用的语法,通过对比Java并结合本人入门的过程和经验,帮助有需求的同学快速掌握OC基本编程,为IOS的入门奠定语言基础。下面首先是写出第一行代码,恭喜正式进入OC学习阶段。
int main(int argc, char *argv[]) { @autoreleasepool //创建自动内存释放池 { //打印输出 NSLog(@"hello world ios!"); return 0; } }
下面介绍OC代码的文件扩展名:
文件扩展名 | 类型 |
---|---|
.h | 头文件,作用是对类、属性、变量、函数等的声明 |
.m | 实现文件,对头文件的生命实现或者扩展 |
.mm | 实现文件,一般是C++代码 |
如果实现文件需要引入头文件时,推荐使用#import
,跟#include
作用相同,优化了确保相同文件只会被引入一次,所以倾向用#import。
基本数据类型
包括:int float double char
类型 | 字节数 | 格式化输出 |
---|---|---|
char | 1 | %c |
int | 4 | %i,%x,%o |
unsigned int | 4 | %i,%x,%o |
short int | 2 | %hi,%hx,%ho |
unsigned short int | 2 | %hi,%hx,%ho |
long int | 8 | %li,%lx,%lo |
unsigned long int | 8 | %lu,%lx,%lo |
long long int | 8 | %lli,%llx,%llo |
unsigned long long int | 8 | %llu,%llx,%llo |
float | 4 | %f |
double | 8 | %f |
long double | 16 | %Lf |
其他数据类型
id类型
可以存放任何数据类型的对象,类似Java中的Object类,其被定义为指向对象的指针(本身就是指针了),故定义比如id instance = nil;id类型是多态和动态绑定的基础。
BOOL类型
布尔值为YES/NO或1/0。Java对应是true/false
nil和Nil
nil相当于Java中的null,表示一个对象,这个对象的指针指向空。Nil是定义一个指向空的类而不是对象。
NSString(不可变字符串)
字符串是非常重要常用的,务必要掌握常用的基础用法,包括创建、截取、遍历、比较、大小写转换、搜索等,语义跟基本类似Java。
//字符串 NSString *str1 = @"ABC3456789"; //拼接成新的字符串 NSString *str2 = [str1 stringByAppendingString:@"wwww"]; NSLog(@"str = %@", str2); //遍历 for (int i = 0; i < [str2 length]; i++) { char temp = [str2 characterAtIndex:i]; NSLog(@"字符串第 %d 位输出 %c", i, temp); } //比较 // sEqualToString方法 :比较字符串是否完全相等,大小写不一样也无法完全匹配。 //hasPrefixe方法:逐一匹配字符串头部。haSuffix方法:匹配字符串的尾部 if ([str2 isEqualToString:str1]) { NSLog(@"相等"); } if ([str2 hasPrefix:@"www"]) { NSLog(@"有该头部"); } if ([str2 hasSuffix:@"www"]) { NSLog(@"有该尾部"); } if ([str2 compare:str options:NSCaseInsensitiveSearch | NSNumericSearch] == NSOrderedSame) { } NSLog(@"比较结果:%d", [str2 caseInsensitiveCompare:str1]); //大小写转换 NSLog(@"str3转大写:%@",[str2 uppercaseString]); NSLog(@"str3转小写:%@",[str2 lowercaseString]); NSLog(@"str3首字母转大写:%@",[str2 capitalizedString]); //字符串截取 NSRange rang = NSMakeRange(2, 2); NSLog(@"str3截取:%@",[str2 substringWithRange:rang]); //搜索 NSRange rang1 = [str2 rangeOfString:@"www"]; NSLog(@"location: %d,length: %d",rang1.location,rang1.length); //替换 //全部替换 NSString *str3 = [str2 stringByReplacingOccurrencesOfString:@" " withString:@"@"]; NSLog(@"替换后字符串为%@", str3); //局部替换 NSString *str4 = [str2 stringByReplacingCharactersInRange:rang withString:@"met"]; NSLog(@"替换后字符串为%@", str4);
NSMutableString(可变字符串)
创建对象的基本写法是[[NSMutableString alloc]init],*号代表对象,[]代表方法调用,只能通过类或者对象才能调用。[NSMutableString alloc]类似Java中new得到一个对象,然后再调用init初始化方法。
//创建对象并初始化 NSMutableString *mStr = [[NSMutableString alloc]init]; //appendstring:向字符串尾部添加一个字符串。 //appendFormat:向字符串尾部添加多个类型的字符串,可以添加任意数量与类型的字符串。 [mStr appendString:@"hello world!"]; NSLog(@"字符串创建%@", mStr); [mStr deleteCharactersInRange:[mStr rangeOfString:@"hello"]]; //删除 NSLog(@"字符串删除%@", mStr); //插入 [mStr insertString:@"love you" atIndex: mStr.length]; NSLog(@"字符串插入%@", mStr);
NSInteger、NSUInteger和NSNumber
NSInteger不是一个对象,而是基本数据类型中的typedef,NSUInteger是无符号的。 当需要使用int类型的变量时,推荐使用NSInteger,这样不需要考虑设备是32位或者64位。NSNumber是一个类,用于包装基本数据类型成为对象,可以理解为Java中的装箱,为一些集合只能存放对象使用,通过字面量方式非常方便将基本数据类型转成对应的对象。例如:
//包装 NSNumber *intNumber = [[NSNumber alloc]initWithInt:43]; //或者字面量方式 NSNumber *intNumber1 = @43; //还原基本数据类型,解包 NSLog(@"%d",[intNumber intValue]);
集合
集合不能接受nil,nil是作为集合结束标识符。
1. NSArray(不可变)
类似Java中的ArrayList,可以存储不同类型的对象,一般情况下数组元素的类型是相同的,特点是有序、可重复。下面展示一位数组的基本操作:
//字面量创建方式 NSArray *arr2 = @[@"aaa",@"bbbbb"]; //工厂方法创建 NSArray *array = [[NSArray alloc] initWithObjects:@"1", @"2", nil]; //取最后一个元素 [array lastObject]; // 取第一个元素 [array firstObject]; // 数组是否包含某个元素 [array containsObject:@"1"]; // 数组的大小 int count = (int) array.count; // 第一种方式遍历 for (int i = 0; i < count; i++) { NSString *_str = [array objectAtIndex:i]; }
那么数据要求是多维的呢?多维数组可以理解为数组的数组,通过嵌套的方式,创建如下:
// 字面量创建二维数组并访问 NSArray *arr2 = @[@[@11, @12, @13], @[@21, @22, @23], @[@31, @32, @33]]; // 字面量访问方式(推荐) NSLog(@"arr2[2][2]:%@", arr2[2][2]); // 数组对象函数访问 NSLog(@"arr2[2][2]:%@", [[arr2 objectAtIndex:2] objectAtIndex:2]);
2. NSMutableArray(可变的)
派生于NSArray,理解为动态数组,提供增加、删除、插入、替换等语法糖。
//创建,当然还有其他方式 NSMutableArray *mutableArr = [NSMutableArray arrayWithObjects:@"one",@"two",@"three", nil]; //添加 [mutableArr addObject:@"hello"]; //替换 [mutableArr replaceObjectAtIndex:2 withObject:@"tihuan"]; //删除 [mutableArr removeObjectAtIndex:1]; //插入 [mutableArr insertObject:@"ios" atIndex:1];
多维数组创建方式如下:
// 初始化作为列的数组,看做4列 NSMutableArray *columnArray = [[NSMutableArray alloc]initWithCapacity:4]; // 初始化2个一维数组,每个一维数组有4个元素,看做1行4列,2行加起来就是2行4列 NSMutableArray *rowArray1 = [[NSMutableArray alloc]initWithCapacity:4]; NSMutableArray *rowArray2 = [[NSMutableArray alloc]initWithCapacity:4]; // 每个行依次增加数组元素 // 第一行 [rowArray1 addObject:@"11"]; [rowArray1 addObject:@"12"]; [rowArray1 addObject:@"13"]; [rowArray1 addObject:@"14"]; // 第二行 [rowArray2 addObject:@"21"]; [rowArray2 addObject:@"22"]; [rowArray2 addObject:@"23"]; [rowArray2 addObject:@"24"]; // 分别打印数组 NSLog(@"myRowArray1: %@", rowArray1); NSLog(@"myRowArray2: %@", rowArray2); NSLog(@"myColumnArray: %@", columnArray);
字典
类似于Java中的HashMap,是一种映射型数据结果,存储键值对,有可变和不可变两种类型。
NSDictionary
主要特点是不可变,如果集合初始化完成,将内容无法修改,无序。
//标准创建 NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:@"cat",@"name1",@"dog",@"name2", nil]; //字面量创建 NSDictionary *dict1 = @{@"name1":@"cat",@"name2":@"dog"}; //第一种遍历 for (NSString *key in [dict1 allKeys]) { NSLog(@"key: %@,value: %@", key, dict1[key]); } //第二种遍历方式,通过遍历器 NSEnumerator *rator = [dict keyEnumerator]; NSString *temp; while (temp = [rator nextObject]) { NSLog(@"%@", temp); } //获取元素 dict1[@"name"]; [dict1 objectForKey:@"name"]; //集合元素的个数 NSInteger count = dict1.count; //沙盒文件存储和读取Plist [dict5 writeToFile:@"路径" atomically:YES]; NSDictionary *dict7 = [NSDictionary dictionaryWithContentsOfFile:@"路径"];
NSMutableDictionary
NSMutableDictionary是NSDictionary的子类。NSMutableDictionary是可变的,动态添加、更改、删除元素,因此不能使用字面量方式(@{})来创建一个可变字典。如果是不可变字典,出现了同名的key,那么后面的key对应的值不会被保存,反之是可变字典,出现了同名的key,那么后面的值会覆盖前面的值。
//创建 NSMutableDictionary *dict = [NSMutableDictionary dictionary]; //添加 [dict setObject:@"dog" forKey:@"name"]; [dict setValue:@"18" forKey:@"age"]; //会将传入字典中所有的键值对取出来添加到dict中 [dict setValuesForKeysWithDictionary:@{@"name1":@"dog"}]; //取元素 [dict objectForKey:@"name"]; dict[@"name"]; //删除 [dict removeAllObjects]; [dict removeObjectForKey:@"name"]; [dict removeObjectsForKeys:@[@"name", @"age"]]; //更新,如果利用setObject方法给已经存在的key赋值,新值会覆盖旧值 [dict setObject:@"20" forKey:@"age"]; dict[@"age"] = @"30";
NSSet && NSMutableSet
具有很好的存取和查找功能,与NSArray相比NSSet的元素没有索引,特点是无序,不可重复,类似Java中的HashSet,其中NSMutableSet提供计算交并集的方法。
NSSet存储元素的过程:
注意:推荐使用字面量方式创建对象,可以缩短代码长度,增加可读性。但是在创建数组的时候要注意,如果含有nil就会抛异常,因为字面量实际上”语法糖“,效果等同于先创建一个数组,然后再把所有的对象添加进来,保证数组不添加nil。
消息传递
前言提到Objective-C最大特点之一是继承了Smalltalk消息传递模型,因此在OC中的方法调用准备的说法是消息传递,类别与消息关系松散,调用方法是给对象发送消息,而方法是对消息的回应,所有消息的处理直到运行时(即runtime)才会动态确定,并交由类自行决定如何处理收到的消息。总结是一个类不保证一定会回应收到的消息,当收到的一个无法处理的消息,会抛出异常。
Java或者C++方法调用:
obj.method(argument);
OC方法调用:
[obj method: argument];
我们都知道在Java或者C++中,如果类没有定义method方法,那么编译肯定不会通过,但是在OC中,理解是发送method的消息给obj,obj收到消息后再决定如何回应消息,如果类内定义了method方法则运行,反之不存在运行期抛出异常。
类
所有面向对象的编程都有类的概念,用于封装数据,这样的语言特性都有封装、继承和多态。OC对象是类在运行期的实例,包含了类声明的实例变量、内存拷贝、类成员的指针等。由于OC是C语言的超集,类由两个部分组成,分别是定义(interface)和实现(implementation),下面举个?。新建一个People类,@interface是接口声明的开始,@end终止结束,所有的OC编译指令都是以”@“开始的。类的实现是通过@implementation指令开头以@end结束。对应People.h和People.m两份文件,下图是类声明(People.h)的展示,主要包括继承关系、成员变量、属性、方法声明等,方法的具体实现是在People.m。
下图是方法声明的展示:
当然不止Interface区块可以定义变量,Implementation区块也可以定义,两者区别是访问权限不一。
前者默认权限为protected,而implementation区块的实体变量则默认为private,所以类别私有可以放在implementation区块。
访问修饰符
- @public:任何位置可以访问。
- @protected:默认情况下成员变量的修饰符。
- @private:变量只限于声明它的类访问,不允许被继承。
- @package:限定在当前包内,类似于Java包的概念。
属性
成员变量是给类内使用的,属性是作为类外访问成员变量的接口,用于封装对象的数据,通过@property声明,编译器自动生成setter和getter方法,此过程称为”自动合成“。类实现文件中@synthesize语法可以指定实例变量的名字,一般不推荐这样做。@dynamic语法是告诉编译器不要自动合成,在OC中访问修饰符很少用到,主要是靠声明属性取值。
属性有五个常用的特质修饰:
assign:针对基本数据类型赋值操作。
strong:定义一种”拥有关系“,属性设置新值时,先保留新值,并释放旧值,然后再将新值设置。
weak:跟strong相反,属性所指的对象销毁时,属性值也会清空。
copy:设置方法不保留新值,而是拷贝一份。
nonatomic:非原子,非线程安全类型。
Q&A:为什么NSString 、 NSArray、 NSDictionary的属性要用copy,集合的深浅拷贝是怎样的?
copy属性作用是为变量赋值的时候系统自动copy一份内存出来,修改新变量不会影响旧变量。在Apple规范中,NSString,NSArray,NSDictonary,推荐使用copy属性,而其NSMubtableString,NSMutableArray, NSMutableDictonary属性则使用strong属性。
NSString *sourceString = [NSString stringWithFormat:@"hello ios"]; //不产生新的内存空间 NSString *copyStr = [sourceString copy]; //产生新的内存空间 NSMutableString *mutableStr = [sourceString mutableCopy]; NSLog(@"sourceString : %@ %p",sourceString,sourceString); NSLog(@"copyStr : %@ %p",copyStr,copyStr); NSLog(@"mutableStr : %@ %p",mutableStr,mutableStr);
使用strong这个属性就有可能指向一个可变对象,如果这个可变对象在外部被修改了,那么会影响该属性。例如:
//代码块 NSMutableString *string = [NSMutableString stringWithString:@"origin"];//copy NSString *stringCopy = [string copy]; NSLog(@"string address is: %p",string); NSLog(@"stringCopy address is: %p",stringCopy);
结果:内存地址不同
NSMutableString *string = [NSMutableString stringWithString:@"origin"]; //NSString *stringCopy = [string copy]; NSString *stringCopy = string; [string appendString:@"change"]; NSLog(@"string address is: %p",string); NSLog(@"stringCopy address is: %p",stringCopy);
结果:内存地址相同
结论:
可变对象指向不可变对象会导致不可变对象的值被篡改,所以需要copy属性。用@property声明NSString、NSArray、NSDictionary 经常使用copy关键字,是因为他们有对应的可变类型NSMutableString、NSMutableArray、NSMutableDictionary,彼此之间可能进行赋值操作,为了不可变对象中的内容不会被无意间变动,应该在设置新属性值时拷贝一份。
浅拷贝:
在Java中浅拷贝如果是基本数据,则拷贝的是基本数据的值;如果是对象,则拷贝的是内存地址,修改该对象会影响另外一个对象。在OC中是对指针的拷贝,拷贝后的指针和原本对象的指针指向同一块内存地址,故同样会相互影响。
深拷贝:
OC中不仅拷贝指针,而且拷贝指针指向的内容,指针指向不同的内存地址,故修改不会相互影响原本对象。
非集合类对象中:对immutable对象进行copy操作,是指针复制(浅拷贝),mutableCopy操作时内容复制;对mutable对象进行copy和mutableCopy都是内容复制(深拷贝)。
方法
通过”+“、”-“分别声明类方法和实例方法,方法如果带有多个参数,参数在方法名之后接冒号定义,多个参数由空格隔开,如果参数个数可变,使用逗号接省略号。例如:
//无参数 - (void)print; //有参数 - (void)print:(int)a andB:(int)b;
构造方法
第一种是重写init方法,第二种是自定义。
/** 重写初始化方法 **/ - (instancetype)init { self = [super init]; if (self) { _peopleName = @"hello ios"; } return self; } /** 自定义初始化方法 **/ - (instancetype)initWithNameAndAge:(NSString *)name andAge:(int)age { self = [super init]; if (self) { _peopleName = name; _peopleAge = age; } return self; }
创建类对象
所有对象和类的引用都是通过指针实现,严格地说指针就是一个地址,是一个常量,而指针变量可以被赋值不同的指针值,创建的对象就是一个指针变量,通过[People alloc]创建一个People对象,分配了内存,init是初始化对象。构造方法有两种方式,第一种是重写init方法,第二种是自定义。
People *p1 = [[People alloc] init]; //调用自定义的构造方法 People *p3 = [[People alloc] initWithNameAndAge:@"mingzi" andAge:12]; //调用方法 [p3 print];
在OC 2.0中,如果创建的对象不需要参数,可以直接使用new:
People *p1 = [People new];
self
作为OC的一个关键字,代表当前类的对象,类似Java中的this,最大的作用是让类中的一个方法调用该类另外一个方法或者成员变量,可以理解”当前类谁调用了这个方法,self就代表谁“。
继承
同Java一样只能单继承,只允许最多有一个直接父类。例如:定义一个父类Computer和子类MacBook。注意方法重写类似Java,子类要重写父类方法不需要重新声明重写方法,在实现部分直接重写目标方法即可。如果需要子类调用父类的方法,可以通过super关键字调用。
//Computer.h文件 #import <Foundation/Foundation.h> @interface Computer : NSObject @property(nonatomic,strong)NSString *name; -(void)calculate; @end // Computer.m #import "Computer.h" @implementation Computer @synthesize name; -(void) calculate{ NSLog(@"i can calculate"); } @end // MacBook.h #import "Computer.h" @interface MacBook : Computer @end // MacBook.m #import "MacBook.h" @implementation MacBook @end //main.m int main(int argc, char *argv[]) { @autoreleasepool { MacBook *macBook = [[MacBook alloc] init]; macBook.name = @"mac"; [macBook calculate]; } }
多态
封装、继承和多态是面向对象编程语言的三大特性,OC的多态是不同对象对同一消息的不同响应方式,实际过程主要分为三种:
- 继承
- 重写
- 指向子类的指针指向父类
可以看出跟Java的多态类似,理解起来应该比较容易,注意是没有方法重载的,在OC中不允许。
Runtime
实例:用Runtime新增一个类Person, person有name属性,有sayHi方法
#import <UIKit/UIKit.h> #import "AppDelegate.h" #import <objc/runtime.h> #import <objc/message.h> void sayHi(id self, IMP _cmd, id some) { //self指的是调用该方法传过来的类 NSLog(@"%@说:%@,我%@岁", [self valueForKey:@"name"], some, object_getIvar(self, class_getInstanceVariable([self class], "_age"))); } int main(int argc, char *argv[]) { @autoreleasepool { //该方法动态创建一个类,arg1:继承自哪个类 arg2:新建类的名称 arg3:extraBytes Class Person = objc_allocateClassPair([NSObject class], "Person", 0); //添加两个实例变量name和age,arg2:变量名称,arg3:内存地址大小,arg5:变量类型 class_addIvar(Person, "_name", sizeof(NSString *), log2(sizeof(NSString *)), @encode(NSString *)); class_addIvar(Person, "_age", sizeof(int), sizeof(int), @encode(int)); //注册方法名 SEL s = sel_registerName("say:"); //arg3:IMP是“implementation”的缩写,这个函数指针决定了最终执行哪段代码 //arg4:方法的参数及返回值 class_addMethod(Person, s, (IMP)sayHi, "v@:@"); //通过该类创建一个实体的对象 id peopleInstance = [[Person alloc]init]; //给对象的 name 实例变量赋值,下面是第二种赋值方式 [peopleInstance setValue:@"XQM" forKey:@"name"]; //Ivar nameIvar = class_getInstanceVariable(Person, "_name"); //object_setIvar(peopleInstance, nameIvar, @"XQM"); //获取实例变量 Ivar ageIvar = class_getInstanceVariable(Person, "_age"); //为变量赋值 object_setIvar(peopleInstance, ageIvar, @21); //调用sayHi方法,arg2:注册指定的方法;arg3:带上有一个字符串的参数 ((void(*)(id, SEL, id))objc_msgSend)(peopleInstance, s, @"大家好"); //调用完成,将对象置为空 peopleInstance = nil; //通过 objc 销毁类,销毁的是一个类不是对象 objc_disposeClassPair(Person); } }
主要流程是:
定义类的方法->objc_allocateClassPair创建类->class_addIvar给类添加成员变量->sel_registerName注册方法名->class_addMethod给类添加定义的方法->注册该类->创建类对象->class_getInstanceVariable获取成员变量,并通过object_setIvar赋值->objc_msgSend调用方法->释放对象,销毁类
Category(类别)
Objective-C借用并扩展了Smalltalk实现中的"分类"概念,用以帮助达到分解代码的目的。类别主要特点是不能增加属性或者成员变量、增加类功能和分离类实现,举个例子: 在UIImageView增加了图片异步加载的功能
@interface UIImageView (ImageViewLoader) <AysncImageDownloaderDelegate> - (void)setOnlineImage:(NSString *)url placeholderImage:(UIImage *)image withRow:(NSNumber *)row; @end @implementation UIImageView (ImageViewLoader) - (void)setOnlineImage:(NSString *)url placeholderImage:(UIImage *)image withRow:(NSNumber *)row; { self.image = image; AsyncImageDownLoader *downloader = [AsyncImageDownLoader sharedImageDownloader]; [downloader startWithUrl:url delegate:self withRow:row]; } @end
Extension(拓展)
拓展也经常用到,主要特点是增加ivar、用于接口分离等。例如:ViewController的实现文件增加@interface ViewController (),支持定义属性等。
@interface ViewController () @property (nonatomic, copy) block b; @end @implementation ViewController @end
异常处理
OC的异常处理极其类似Java中的,包括4个指示符,分别是@try、@catch、@throw、@finally。可能存在异常的代码写在@try块,异常处理逻辑写在@catch,@finally块的代码总是要执行的,@throw作用是抛出异常。
协议
类似Java中的接口(interface),类似多重继承功能,支持协议继承协议,通过定义一系列方法,然后由遵从协议的类实现这些方法,协议方法可以用@optional关键字标记为可选,@required关键字标记为必选,编译器会出现检查警告,一般来说还是可以编译通过。下面看下语法:
@protocol ClickDelegate - (void)click; - (void)unclick; @end
协议最常应用在委托,分为委托方和代理方,委托方负责定义协议、通过id类型持有协议和调用协议的方法,而代理方则遵从协议、设置协议代理对象以及实现协议方法即可。
block
类似Java中的Lambda表达式,比较复杂,笔者的理解还未达到一定解说程度,所以这里先不做解释,放到后续的文章中介绍。
内存管理
Java的内存管理是由垃圾回收器负责,OC中引入自动引用计数(ARC),内存管理交由编译器决定。引用计数是每个对象都有一个计数器,如果对象继续存活,计数器递增其引用计数,用完之后递减引用计数,如果计数变为0,表示对象可以被释放了。NSObject协议声明了Retain、release和autorelease方法用于操作计数器,分别是递增、递减、自动释放操作,所有的对象都是收集器的工作对象。
ARC:自动引用计数,编译器自动生成retain/release
MRC:手动管理引用计数,旧版本使用
autoreleasepool:延迟自动释放
strong/weak/assgin最佳实践
基本类型:assgin;
delegate->week;
集合和block用copy;
其他用strong;
block中的self用weak打破循环引用。
参考资料
https://www.jianshu.com/p/eb713b1f22dc
https://www.jianshu.com/p/6ebda3cd8052
https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtForwarding.html
原文链接:https://blog.csdn.net/xiaoming100001/article/details/94337406