图片 1泰妍

  • SEL是方法或者函数指针吗?
  • 方法签名是什么,有什么用处?
  • 为什么方法转发需要先返回一个方法签名?
  • 除了runtime方法外你会如何调用私有方法?
  • 为什么OC没有方法重载的概念?

图片 2

背景

今天做地址列表页面,如图:

图片 3

tableView的约束我是这样写的:

[self.tableView mas_makeConstraints:^(MASConstraintMaker *make) { make.top.mas_equalTo(myAddressLabel.mas_bottom); make.left.right.bottom.mas_offset;}];

tableView滚动的时候是这种效果:

图片 4效果.gif

但是当tableView滚动到最底部的时候,最后一个cell被挡住了:

图片 5home_indicator挡住了cell的内容

于是我针对iOS 11调整了一下约束:

[self.tableView mas_makeConstraints:^(MASConstraintMaker *make) { make.top.mas_equalTo(myAddressLabel.mas_bottom); make.left.right.mas_offset; if (@available(iOS 11.0, *)) { make.bottom.mas_equalTo(self.view.mas_safeAreaLayoutGuideBottom); } else { make.bottom.mas_equalTo(self.view); }}];

OK,现在就不会挡住最后一个cell了:

图片 6home_indicator不会挡住cell的内容

但是滚动tableView的时候又尴尬了:

图片 7尴尬.gif

对比这张GIF与上一张GIF,可以发现这里根本没有体现iPhone X全面屏的优势。

真正的适配iPhone
X,是滚动的时候全屏滚动,滚到底的时候最后一个cell也不会被home_indicator挡住。

iOS开发中我们整日跟方法打交道,我们都知道它最后都是发送该消息,它用起来足够简单,但对于方法调用涉及到的一些知识和概念我觉得有必要再次认识一下,接下来的篇幅我将要介绍

去年,我在微博上发起了100天阅读博文的行动。具体就是,每天读一篇 iOS
开发相关的技术博文,并在微博上分享自己的读后感。对于在开发、工作、和面试中常见的问题,通过这段时间100篇博客的阅读,我产生了自己的一些心得体会,分享如下。至于想看博客原文的朋友,文末也有相关链接。

写了个demo研究

为了方便弄清问题我新建了一个小demo,demo里我并没有对约束做什么特殊处理:

UITableView *tableView = [[UITableView alloc] init];[self.view addSubview:tableView];tableView.dataSource = self;tableView.delegate = self;[tableView mas_makeConstraints:^(MASConstraintMaker *make) { make.top.mas_offset(NAVIGATION_BAR_HEIGHT); make.left.right.bottom.mas_offset;}];

demo的效果:

图片 8demo.gif

这才是我想要的效果啊!

我很疑惑,同样的约束代码我用在项目里怎么就不对了。

  • SEL
  • IMP
  • Method
  • NSMethodSignature
  • NSInvocation

第一个感触就是iOS开发真是博大精深。我一开始的文章来源定为Medium、Swift
Summit、Realm、Apple
WWDC。这些渠道都是经过筛选的上佳博文和视频,而且话题广泛,涉及基础架构、语法框架、性能优化、开源等方方面面。

当初为了适配iOS 11,你做了什么?

后来我把这个问题发到群里,我同学问我是不是用了automaticallyAdjustsScrollViewInsets

这个属性我有用到,但是iOS 11出来后我就没用了,我用了另一个:

contentInsetAdjustmentBehavior

iOS 11刚出来的时候,我发现项目中的scrollView在iOS
11虚拟机上的偏移量发生了变化,那个时候关于适配iOS
11的文章很多,我也很轻松的找到了解决方案:

  • 在AppDelegate.m中加上这段代码:

if (@available(iOS 11, *)) { [UIScrollView appearance].contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever;}

scrollView在iOS
11上的偏移问题就解决了,这段代码是什么意思我并未研究,只是从其字面意思上感觉它就是让scrollView不要发生偏移,但是我并没意识到它跟安全区域有关

SEL只是方法的名称

在运行时objc.h中可以看到如下定义,SEL是一个指向objc_selector结构体的指针,SEL可以看似是人的名字

/// An opaque type that represents a method selector.typedef struct objc_selector *SEL;

在运行时源码里面我们并没有看到objc_selector结构体的具体实现,但根据我们的打印数据我们可以认为objc_selector内部至少包含一个c字符串的字段,可能还包含其他用来加快SEL查找的辅助字段。

  • ###### SEL的三种创建方式

SEL s1 = @selector;SEL s2 = NSSelectorFromString;SEL s3 = sel_registerName;NSLog(@"s1 %p", s1);NSLog(@"s2 %p", s2);NSLog(@"s3 %p", s3);

[3057:414576] s1 0x10b0ee595[3057:414576] s2
0x10b0ee595[3057:414576] s3
0x10b0ee595打印输出三个SEL变量的值,可以看到三个变量的值是一样的,这是因为SEL是存储在静态数据区,像字符串常量一样只要是名称一样的方法他们的sel都会是同一份内存,所以SEL并不依赖于方法而存在,可以创建一个SEL但是整个App可以没这个方法存在。

即使来自不同类的或者不同模块的,只要方法名称相同(比如'setName:'就是一个方法名称),那么这些方法的SEL的值都一样,所有的SEL都会由全局变量来维护;使用@selector宏的方式创建SEL的一个好处是在编码阶段它会在当前上下文环境中寻找对应的名称的方法,所以如果查找不到该方法会则Xcode会给出警告提示;

虽然我在开始做100天博文阅读之前只确定了30到40篇待读文章,但是每读完1篇文章,网站又会给我推荐1到2篇相关或延展阅读;同时我又会对文章中不清楚的地方进行思考和搜索,这就又发现了不少很好的博文博客,例如喵神的博客、微信读书团队的博客、以前忽视的WWDC视频。久而久之,100篇就不知不觉得积累到了。

绕不过的安全区域

关于安全区域的文章有很多了,相信大家就算没仔细研究过,零散的文章也读了不少,这里给大家推荐一篇:

现在来说下[UIScrollView appearance].contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever和安全区域的关系。

To customize the appearance of all instances of a class, send the
relevant appearance modification messages to the appearance proxy for
the class. For example, to modify the bar tint color for all
UINavigationBar instances:[[UINavigationBar appearance]
setBarTintColor:myColor];

自己翻译:自定义所有这个类的对象的外观,比如说修改所有导航栏的bar tint color:

 [[UINavigationBar appearance] setBarTintColor:myColor];

图片 9

自己翻译:是否根据安全区域调整偏移量,默认是自动调整的。

[UIScrollView appearance].contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentNever这句话的意思就是所有scrollView的偏移量不随安全区域而调整。

这就是项目里的scrollView翻到底的时候最后一个cell会被home_indicator挡住的原因。

又因为contentInsetAdjustmentBehavior的默认值是UIScrollViewContentInsetAdjustmentAutomatic,所以小demo里tableView自动调整了偏移量,因此翻到底的时候最后一个cell不会被home_indicator挡住。

所以要解决项目里的tableView的显示问题,只需要将这个tableView的contentInsetAdjustmentBehavior改为UIScrollViewContentInsetAdjustmentAutomatic

if (@available(iOS 11.0, *)) { self.tableView.contentInsetAdjustmentBehavior = UIScrollViewContentInsetAdjustmentAutomatic;}[self.tableView mas_makeConstraints:^(MASConstraintMaker *make) { make.top.mas_equalTo(myAddressLabel.mas_bottom); make.left.right.bottom.mas_offset;}];

图片 10修改后.gif

IMP 是方法的函数指针

IMP定义如下:

#if !OBJC_OLD_DISPATCH_PROTOTYPEStypedef void (void /* id, SEL, ... */ ); #elsetypedef id (id, SEL, ...); #endif

可以看到IMP是一个拥有多参数的函数指针,OC中的所有的方法调用最后都将转换为IMP指针指向的函数调用

最后做到第100天的时候,我还剩下几十篇文章特别想去研究阅读、还有很多疑惑想去思考总结,在iOS开发中,好的文章总是层出不穷。即使我读的100篇博文数量不少、涉猎广泛,还有例如逆向开发、安全、持续集成、测试等方面我没有机会去研究。这么多值得探索的地方,恰恰说明,iOS开发远没有到了没人要的地步,而我在这方面的学习也远远没有到达尽头。

反思

最初适配iPhone X的时候,我的想法很简单:iPhone
X嘛,无非就是状态栏和tabbar的高度发生了变化,多了一个home_indicator而已,几个宏就搞定了:

// 判断是否是iPhone X#define iPhoneX ([UIScreen instancesRespondToSelector:@selector(currentMode)] ? CGSizeEqualToSize(CGSizeMake(1125, 2436), [[UIScreen mainScreen] currentMode].size) : NO)// 状态栏高度#define STATUS_BAR_HEIGHT (iPhoneX ? 44.f : 20.f)// 导航栏高度#define NAVIGATION_BAR_HEIGHT (iPhoneX ? 88.f : 64.f)// tabBar高度#define TAB_BAR_HEIGHT (iPhoneX ? (49.f+34.f) : 49.f)// home indicator高度#define HOME_INDICATOR_HEIGHT (iPhoneX ? 34.f : 0.f)

凭借这几个宏,我把导航栏和tabbar高度一改,然后就想当然的认为完成了iPhone
X的适配工作——在别人还在研究安全区域的时候。

当然,tableView是这样的:

图片 7不是全面的全面屏.gif

我想的是:home_indicator没有挡住cell内容就是适配完成了。(还好APP的几个主页都是有tabbar的,不存在这种问题,不然真的丑爆了。)

安全区域是什么?我知道有这个东西,这东西重要吗?反正也不用,懒得去管。

事实说明人终将为自己的无知付出代价。

诚如CepheusSun所说:

在适配 iPhone X 的时候首先是要理解 safe area 是怎么回事,盲目的 if
iPhoneX{} 只会给之后的工作带来更多的麻烦。

Method是SEL和IMP的一个映射关系的包装

  • ###### Method定义

typedef struct old_method *Method;struct objc_method { SEL method_name OBJC2_UNAVAILABLE; char *method_types OBJC2_UNAVAILABLE; IMP method_imp OBJC2_UNAVAILABLE;} OBJC2_UNAVAILABLE;
  • ###### 方法的获取

Method class_getInstanceMethod(Class cls, SEL sel) 
  • ###### 从方法列表中查找方法过程

static inline old_method *_findMethodInList(old_method_list * mlist, SEL sel) { int i; if  return nil; for (i = 0; i < mlist->method_count; i++) { old_method *m = &mlist->method_list[i]; if (m->method_name == sel) { return m; } } return nil;}

解析:在类的结构中包含有它的实例方法列表(Method列表),而Method的结构体中包含方法选择子SEL和方法实现IMP
runtime的方法查找过程,简化起来就是根据SEL在类的方法列表中遍历查找与之匹配的SEL的方法。

这种查找过程也决定了OC语言不支持方法重载这个特性。如果支持重载,包含不止一个同名方法,那么该方法的查找只会找到第一个就直接返回,因为根据SEL不能确定是查找哪个重载方法;
  • ###### 至此我们能回答”为什么OC没有方法重载?”这个问题了

重载方法是【方法名称一样,但是参数个数,参数类型不一样的方法】,在这样的前提下,我们要确定一个方法就得需要方法名称SEL和方法参数类型描述method_types这两个条件了,上面方法查找的过程_findMethodInList我们可以看到运行时查找方法仅仅根据方法名SEL来查找的if (m->method_name == sel),所以OC目前是不支持方法重载这个很多语言都有的特性的;

  • ###### 方法签名的本质

+ (nullable NSMethodSignature *)signatureWithObjCTypes:(const char *)types;

从方法签名的生成方法可以看到方法签名只需要一个类型参数字符串就可以构造,这个类型参数是OC中通用的类型编码,字符串格式为:返回参数类型参数1类型参数2类型,比如setName(NSString *)name的类型字符串为v@:

  • ###### 类型编码 type Encodings

runtime中为了表达方便,对各种数据类型的表示进行了编码,所有的类型都可以用一个对应的字符来表示,其中v表示void类型,@表示OC对象类型,:表示SEL类型,具体可参考官方文档Type
Encodings

// 类型编码枚举enum _NSObjCValueType { NSObjCNoType = 0, NSObjCVoidType = 'v', NSObjCCharType = 'c', NSObjCShortType = 's', NSObjCLongType = 'l', NSObjCLonglongType = 'q', NSObjCFloatType = 'f', NSObjCDoubleType = 'd', NSObjCBoolType = 'B', NSObjCSelectorType = ':', NSObjCObjectType = '@', NSObjCStructType = '{', NSObjCPointerType = '^', NSObjCStringType = '*', NSObjCArrayType = '[', NSObjCUnionType = '(', NSObjCBitfield = 'b'}// 练习一下类型编码// - setName:(NSString *)name; // - (NSString *)name; // - downloadImage:(NSString *)url completionHandler:^(UIImage *image))completionHandler; // v@:@// @@:// v@:@^

// 测试代码NSMethodSignature *methodSignature1 = [NSMethodSignature signatureWithObjCTypes:"v@:"];NSMethodSignature *methodSignature2 = [NSMethodSignature signatureWithObjCTypes:"v@:"];NSLog(@"methodSignature1 %p", methodSignature1);NSLog(@"methodSignature2 %p", methodSignature2);// 输出// 2018-01-29 22:18:34.622 IANLearn[3057:66145] methodSignature1 0x608000266ec0// 2018-01-29 22:18:34.622 IANLearn[3057:66145] methodSignature2 0x608000266ec0
方法签名是用来表达一个方法的参数特征,这些特征包含方法的参数个数,参数类型,返回值类型和SEL一样,所以只要是方法的参数特性一样,那么方法的签名就一样,所有的方法签名都会由全局变量来维护
  • ###### NSMethodSignature查找

- (NSMethodSignature *)methodSignatureForSelector:aSelector;+ (NSMethodSignature *)instanceMethodSignatureForSelector:aSelector;// 测试代码NSMethodSignature *methodSignature3 = [self methodSignatureForSelector:@selector(application:didFinishLaunchingWithOptions:)];NSMethodSignature *methodSignature4 = [AppDelegate instanceMethodSignatureForSelector:@selector(application:didFinishLaunchingWithOptions:)];NSMethodSignature *methodSignature5 = [AppDelegate instanceMethodSignatureForSelector:@selector(unknowMethod)];NSLog(@"methodSignature3 %p", methodSignature3);NSLog(@"methodSignature4 %p", methodSignature4);NSLog(@"methodSignature5 %p", methodSignature5);// 输出// 2018-01-29 22:28:11.792 IANLearn[3207:70694] methodSignature3 0x600000263900// 2018-01-29 22:28:11.792 IANLearn[3207:70694] methodSignature4 0x600000263900// 2018-01-29 22:28:11.793 IANLearn[3207:70694] methodSignature5 0x0

上面两个方法都是从某个类中查找指定SEL的方法签名,整个过程大概是是去对应的类方法列表中寻找与该SEL匹配的方法Method,然后直接返回该Method的签名,如果没有查找到该方法则返回nil

有一点需要说明的是在其他语言里面方法签名包含方法名称和参数信息,而OC的NSMethodSignature 仅仅包含参数信息

第二个感触就是做一件事情坚持100天真是不容易。无论是选题、阅读,还是最后写读后感,都是一件劳神劳力的事情。而在这100天每天都花时间阅读,要确保其中从不间断,无论这天发生什么事情,确实十分劳累。

最后,再次强调

真正的适配效果是:

图片 12正确姿势.gif

不是:

图片 13home_indicator挡住了最后一个cell.gif

也不是:

图片 14这还是全面屏吗?.gif

希望我们都能做一个格物致知的开发者。

NSInvocation是OC的方法调用器

OC语言中有多种方式去调用方法,抛开c/c++的调用方式可以列举出如下几种:

  1. 对象/类对象直接调用方法[obj method:param1:],这种方式是最面向对象的姿势了;
  2. [peformSelector withObject:],这种方式它可以很方便的去延迟调用,或者丢到后台线程调用,并且调用没有定义的SEL也不会导致编译错误,但它最大的问题是最多传递2个参数,没有返回值,并且参数只能是对象类型的所以不是很灵活;
  3. block调用 block(name, age)
    ,闭包可以看做是匿名方法,它的调用不用去对象类中去寻找方法,本身block的结构中就包含方法的实现,它其中的方法不属于某个对象只属于闭包本身;4.invocation调用,invocation调用是OC中最灵活的方法调用方式,它可以调用对象的私有方法,可以传递任意多个参数,可以兼容各种参数类型,并且可以存储方法调用的返回值。
  • ###### invocation的构建

+ (NSInvocation *)invocationWithMethodSignature:(NSMethodSignature *)sig;

一个NSInvocation主要由方法签名+参数值来确定,
通过方法签名创建NSInvocation后我们给他的参数值就行,其中调用对象是第一个参数,方法名称SEL是第二个参数

  • ###### invocation的调用

- invoke;- invokeWithTarget:target;

NSInvocation的调用有2个方法,target参数可以直接设置target属性或者设置为第一个参数,如果不设置则调用第二个方法将target传入

// 测试// 构建对象 测试UILabel的setText:方法UILabel *myObj = [UILabel new];NSLog(@"invocation执行前myObj.text=%@", myObj.text);// 构建方法签名返回类型void编码为v,对象UILabel类型编码为@,SEL编码为:,参数类型NSString编码为@NSMethodSignature *myMethodSignature = [NSMethodSignature signatureWithObjCTypes:"v@:@"];// 构建NSInvocationNSInvocation *myInvocation = [NSInvocation invocationWithMethodSignature:myMethodSignature];// 设置第1个参数myInvocation.target = myObj;// 设置第2个参数myInvocation.selector = @selector;// 设置第3个参数NSString *newText = @"change new text";[myInvocation setArgument:&newText atIndex:2];[myInvocation retainArguments];// 执行[myInvocation invoke];NSLog(@"invocation执行后myObj.text=%@", myObj.text);
  • ###### invocation的使用场景
  1. 调用多参数的私有方法私有方法我们不能通过对象直接调用,我们可以使用peformSelector的方式调用,但是对于多余2个参数的情况我们就没办法调用了,这时候就可以构建NSInvocation来调用了。
  2. 方法转发调用方法转发的forwardInvocation中需要我们去执行NSInvocation,通常是将NSInvocation通过更改target参数的形式转发给其他对象来执行。

参考资料:

一开始的阅读因为是我一直关注的话题,加上我又有iOS开发的基础,所以理解起来很快,顶多算是知识的梳理和总结。后来看的博文涉及全新的知识点,加上100天中间WWDC如期而至,大量的更新迅速将我那些微薄的老本吃光。这个时候就捉襟见肘了,每天花在阅读和理解的时间从1个小时猛增到3个小时左右,然后码字也越来越多——因为思考的时间相对较短,所以写出来自然不够精炼。

2018年10月8日更新

说明:上面判断iPhone X的方法过时了,具体可参考——

判断是否是iPhone X系列机型

在这么多困难之下,我坚持下来的原因不是因为我多么有毅力,而是因为每天大家对我博文的积极评价,这就要说到下一个话题了。

第三个感触就是大家的反馈。无论写什么,总有网友和开发者朋友和我一起阅读全文,并对我的读后感进行评价。很多观点让我茅塞顿开,也有很多点赞和转发让我充满感激。

看着小伙伴们这么认真、每天都在积极努力地和我一同学习、研究博文并进行思考,想偷懒的我愈发自惭形愧,敷衍了事和半途而废的心理也就烟消云散了。

发表评论

电子邮件地址不会被公开。 必填项已用*标注