RN学习2——客户端开发者的一些准备工作

从具体开发实践的角度,这里本应该是第三步,因为第二步是我将现有项目进行了RN集成,并实现了一些混编的小功能后,才逐步总结出来的下面一些实践经验。下面的部分先写一下,第三章再介绍如何集成现有项目并进行简单开发。

一、 RN 开发前的一些必要准备技能

如果你是一个客户端开发,要对js和native的交互很熟悉,起码要懂js的语法,了解react和ES6是怎么回事(本人完全不懂react.js,表示实践起来非常吃力)。

如果你是一个前端开发,那你一定要很熟悉react.js,因为你开发主要靠这个了,混编是一个很有门槛的技术点。

这里主要介绍客户端开发应该具备的知识:

  • 1、javascript的基本语法,变量、命名等等。

    如果你是一个老鸟,估计也就10分钟就好了

  • 2、react.js。

    react facebook给js的重新定义。现在还只是0.14.7,表示这还是一个飞速发展而且尚有不足的组件。非常性感,非常前卫,值得学习。从一个客户端开发者的角度(区别于前端),它的基本特点就是增加了class的概念和生命周期的概念,以及component的概念(js逻辑代码直接嵌套html的jsx语法)。

    这里建议用半天到一天时间进行学习和研究,因为一方面毕竟react和react native在很多api上有着不同,更多的应该在实践中摸索,那样提高更快。开发时网页要常开react的官方文档。

  • 3、ECMAScript 6

    前端开发是程序员届的特殊群体,总是能创造出一些从名字上就高端而又气势磅礴的名词和架构,如果你听到一个新的框架(架构?名称?)后一脸懵逼,这就对了…ES6就是这样一个东西,几乎可以说,ES6只保留了javascript旧版本的一些基本特性(这也就是为什么只要10分钟去看了),它几乎把js变成了一个全新的语言。ES6具有现代语言的各种优点和特性,如果你是一个iOS开发者,那么你就会发现它和swift竟然如此相似。

    ES6要花多少时间看我也不清楚,可能需要一本书。。。

    阅读更多

RN学习1——前奏,app插件化和热更新的探索

React Native(以下简称RN)有大量前端开发者的追捧。前端开发是一个活跃的社区,一直尝试着一统前后端,做一个全栈开发,RN就是他们在客户端领域的尝试。

说是从零开始,但其实我还是懂一点点JS代码的,而且算是一个有经验的iOS、Android开发,对很多js和native交互的细节和特性还算了解,在QDaily里面也做过好多hybird的尝试,还经常用JSPatch做hotfix,总的来说,就是对hot update、插件化以及hybird编程非常非常感兴趣。RN也许是已知的开源方案中最好的一个吧。

一、写在最前

先开始提供个思路,作为一个移动客户端开发(区别于前端开发),我使用RN的目的根本上是为了插件化以及插件的线上热更新,对于前端开发那种全应用RN化的雄心是敬谢不敏的,同事对这种方案开发全app的能力也是存疑的(后文会解释原因)。

至于为什么要学习RN,主要是个人以后目标是做一个更加成熟团队的客户端负责人(or客户端架构师),需要对整个客户端横向技术栈都要有自己的理解和认识,现在已经能够在Android和iOS方面有一些自己的认识,进入RN领域其实也是顺理成章的了。

在未来RN的学习和使用过程中,我也会更加倾向于关于这种方案的内在原理、使用场景、使用边界以及一些其它优缺点方面,实际使用中也会先在一些比较轻量级的场景使用。

二、动态配置

客户端的新版本都依赖用户进行升级才行,如何能够第一时间让用户使用最新的版本是app开发者的永恒话题。

如果采用后台热更新,无论采用何种方式,我们的流程总是可以归结为以下三部曲:“从 Server 获取配置 –> 解析 –> 执行native代码”。

针对客户端程序的混合开发非常有必要,可以有效的进行app一些突发模块的开发和处理。已知的一般思路包括:

  • 1、简单的js bridge方式,内容呈现采用H5,增加一些和native的交互。现在好奇心日报就是这个方案,虽然简陋,但基本就是这个思路。上个东家微信在这方面也基本就采用了相似的方案,只不过在加密和安全方面做了更多的处理。

  • 2、后台以zip包得形式下发html、css、js和相关png组件,下载之后将所有资源按照原有目录结构放在app本地的一个http服务器中,app中得webview请求localhost的固定地址进行请求,这种请求会有AJAX跨域问题,一般只有native端完全接管网络请求可以使用,例如微信春晚红包就是这个方案。方案2基本是方案1的优化变种,由于兼容性好,入门简单,比较成熟。

  • 3、以zip包得形式下发html、css、js和相关png组件,客户端深订制一套方案,可以让js使用一些原生的UI组件能力,比较典型的就是增加下拉刷新组件。这套方案在淘宝、支付宝广泛使用,需要进行学习和研究,一方面在部署架构,另一方面是具体实现细节。阿里开源了其中的weex组件,基本思路也是这样的。

  • 4、纯js或者lua以patch的形式进行原生开发,通过反射调起原生代码。这个方案在iOS下可行,Android下面还存疑。而且是比较重的客户端耦合,好奇心日报现在用它进行一些紧急bug的处理。

  • 5、采用react-native进行混编,这种方案比较完整,就是原生native客户端集成react-native组件,通过后台订制下发一些使用js和css编写的资源模块,通过react-native框架进行渲染解析,成为原生应用。方案已经在天猫iPad客户端的某些模块上、QZone的某些使用,而且facebook的f8大会的app全部采用其编写,其生产能力是不容质疑的。方案原理和方案3、方案4类似,只不过中间封装了一个更加完善的中间件。

三、好奇心日报的现有尝试

1、基于css的ui配置方案。

基本思路是在css文件中定义ui组件的边距、颜色、字体、大小、背景色等等与UI相关的内容,在代码中通过宏(Android中用import static)进行UI渲染。

在app启动时,将css文件load进入内存,保存成k-v的形式,具体UI代码直接面对这些k-v数据结构即可。

这种方法的好处是:

  • 1、在适配夜间模式或者一些固定屏幕版本时,只需增加一套css文件即可处理;
  • 2、而且,由于修改资源文件,app不需要重新打包编译;
  • 3、同时,这种方案可以通过后台配置新的资源文件,在运行时替换掉内存中的约定key下的value,从而实现线上条件下的UI调整。

局限性也是非常明显的:对native代码的依赖太硬,能做的非常少,也就能改改样式,与热更新和热修复都扯不上关系。

2、基于js bridge的bybird方案

基于H5的webview hybird方案算是在性能上做一些妥协后比较成熟的方案了。

在android和iOS部分各封装一个js-bridge用于js和native的交互,相当于一个中间件。该中间件包含一个native部分和一个js部分,两部分沟通采用各自平台的特性方案,iOS采用订制request scheme并拦截request的方案,android采用@javascript的方案。通过中间件,前端开发者仅仅使用1套代码就可以兼容两个平台,两个平台各自暴露native方法给前端。

该方案实现的部分有:

  • 1、两个js-bridge,用于交互。并以此约定标准化调用接口。
  • 2、为webview发起的请求绑定cookie,以能够进行用户识别
  • 3、为webview发起的请求定制化UA,以区分浏览器还是app,以及android还是iOS。
  • 4、针对webview中所有资源(html、js、css、image)都进行本地的持久化,以提高访问速度。

方案好处都能看见,缺陷也很明显:效率太依赖机器性能以及浏览器内核(不过就算内核再好效率也是存疑的),同时针对原生部分的调用依赖于原生提供能接口,几乎是每增加一个功能,native部分也需要对应开发一边接口。

以上两个方案各有特征,但终究没离开采用约定好的配置信息就行混合编程的路子。从本质上来说,就是移动端和服务端约定了一套协议,但是协议内容严重依赖于应用内提供的能力,不利于拓展。尤其是方案1,只是在解析字符串,它完全不具备运行和调试的能力。方案2的效率问题也非常明显。

3、jspatch的热修复方案

iOS7以后,系统中包含了jscontext进行js语言的解析,相当于从读取配置文件到读取逻辑一个质的飞跃。

jspatch将js代码进行解析,并通过反射(invoker)调用objective-c的代码,几乎可以做所有oc可以做的事情(因为OC的runtime实在太强大)。

方案在QDaily中主要用于线上热修复。这个方案也有其不好之处:一个是只支持iOS,针对android还是无能为力;二是编写页面实在难用,难以调试,从整个生态来讲,也都使用比较轻量。

react native在iOS端实现远离和jspatch的远离基本一致,同事结合了方案2中语法的一些特性以及方案1中的配置特性。相信是现在已知的最优解决方案。

四、混编

决定学习之初直接上混编,因为这才是使用的目的,只有支持这个才具备插件使用的条件。

开一个官方demo——AwesomeProject,然后开始修改(如何安装和配置请自行google,官方教程很详细)。

1、OC调起RN

直接上代码,我们假设native页面本来好好的,点击了一个按钮跳到了一个RN的活动页面

1
- (void)viewDidLoad {
    [super viewDidLoad];
  	UIButton* startRNVC = [[UIButton alloc] initWithFrame:CGRectMake(20, 50, 60, 40)];
  	[startRNVC setTitle:@"Start RN" forState:UIControlStateNormal];
  	[startRNVC addTarget:self action:@selector(gotoRN) forControlEvents:UIControlEventTouchUpInside];
  	[self.view addSubview:startRNVC];
}

- (void) gotoRN {
  NSURL *jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/index.ios.bundle?platform=ios&dev=true"];
  RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation
                                        moduleName:@"AwesomeProject"
                                               initialProperties:nil
                                                   launchOptions:nil];
  rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1];
  UIViewController *rootViewController = [UIViewController new];
  rootViewController.view = rootView;
  [self.navigationController pushViewController:rootViewController animated:YES]; 
}

####2、OC中等待RN调起的部分
RN有比较完整的调用代码,只要按步骤做就好了。关键是RCT_EXPORT_MODULE这个宏,会在class的load方法中进行register,这点和js-bridge的方案很像。

1
@implementation SpringBoard
RCT_EXPORT_MODULE();

RCT_EXPORT_METHOD(gotoIM:(RCTResponseSenderBlock)callback)
{
  AppDelegate* appdelegate = (AppDelegate*) [UIApplication sharedApplication].delegate;
  UINavigationController *controller = (UINavigationController*)[appdelegate.window rootViewController];
  CDLoginVC *loginVC = [[CDLoginVC alloc] init];
  [controller pushViewController:loginVC animated:YES];
  callback(@[[NSNull null]]);
}

- (dispatch_queue_t)methodQueue
{
  return dispatch_get_main_queue();
}
@end

3、RN部分调起Native

RN还不是很理解,就把代码都贴上来了。这里会在页面启动时候直接alert出来,点击会再跳回native部分。

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
47
48
49
50
51
'use strict';

var React = require('React');

var RN = require('react-native');
var {
Image,
AppRegistry,
ListView,
StyleSheet,
Text,
View,
AlertIOS,
} = RN;


var styles = RN.StyleSheet.create({
container: {
flex: 1,
flexDirection: 'row',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#F5FCFF',
}
});

function setup(): React.Component {
AlertIOS.alert(
'Foo Title',
'My Alert Msg',
[
{text: 'Foo', onPress: function FooClick() {
var SpringBoard = RN.NativeModules.SpringBoard;
SpringBoard.gotoIM((events) => { });
}},
{text: 'Bar', onPress: () => console.log('Bar Pressed!')},
]

)
class AwesomeProject extends React.Component {

render() {
return <View style={styles.container}>
<Text>This is a simple application.</Text>
</View>;

}
}
return AwesomeProject;
}

AppRegistry.registerComponent('AwesomeProject', setup);

总结:事实上,效果很好,轻松实现了混编和调用,考虑到RN在调起Native部分需要OC进行代码定制化编写,所以未来考虑增加其与jspatch的协作,增强其能力;android部分还需要继续研究,相信不是问题(QZone已经在进行相关的研究和应用了)。

五、学习计划和曲线

我个人是一个双平台开发者,同时对hybird编程比较感兴趣,也做过一些研究和尝试,所以RN中关于平台接口部分、原理以及js-native交互部分学习是比较平缓的。但我javascript只是一点三脚猫功夫,更别提ES6、React一个有一个生僻而又让然懵逼的名字,还有node.js等等神一样的存在…这部分估计学习要非常陡峭。

本着先难后易的原则,学习部分会优先进行ES6标准的基本语法和习惯开始,然后通过改造QDaily一个模块进行React和RN的熟悉,在其中不断学习f8的代码和使用方式,并在过程中将RN彻底融入原有app项目中。

这过程可能需要一本基于ES6的javascript的书籍,一套比较权威的RN教程和文档,f8的代码以及针对其的解读,还有若干大牛的博客和社区。

六、本文结束

mark一下本文的参考文献,以及可能要学习的一些东西:

自定义Android开源库CSKit介绍

自定义Android开源库CSKit介绍

整个项目框架包含单例管理(MManagerCenter),网络请求及文件下载(VolleyPlus)和事件总线管理(MMBus)的一个集合框架。还处于检验阶段,使用方法(Android studio):

在Project的gradle文件中增加:

1
2
3
4
5
repositories {
maven {
url "http://dl.bintray.com/droison/maven"
}
}

在使用的module增加:

1
compile 'xyz.chaisong.cskit:cskit:0.0.3'

项目的Github地址为http://github.com/droison/CSKit

阅读更多

AFN源码分析-AFHTTPConnectionOperation

AFHTTPConnectionOperation是AFNetWorking在使用NSURLConnection进行网络请求和下载的基本任务单元,其继承自AFURLConnectionOperation,仅对其进行了一些不太复杂的封装。在整个框架中,AFN并没有直接使用过AFURLConnectionOperation,均为面向AFHTTPConnectionOperation的一些开发工作,但作为抽象更好的Operation,需要进行优先的分析与学习。

阅读更多

AFN源码分析-AFURLRequestSerialization

AFURLRequestSerialization

该Class为用于对请求参数进行拼接、http body组合以及header配置。

包含3个具体的实现类:AFHTTPRequestSerializer、AFJSONRequestSerializer、AFPropertyListRequestSerializer,主要针对3种不同的请求参数组合格式。定义了一个protocol–AFMultipartFormData,用于对数据上传使用。本类中一半篇幅进行POST multi上传情况的处理。

所有HTTPHeaderField的参数都通过一个固定的setValue:forValue的方法进行设定,不再赘述。其它还可以设置需要auth的用户名密码、写在body中数据的编码格式等。有一个比较不错的设计:可以通过传入block的形式将参数部分拼接的具体实现抛回给调用方,用以特殊定制。

比较有意思的在于,本类为几个成员变量默认增加了KVO观察者。观察参数包括:allowsCellularAccess、cachePolicy、HTTPShouldHandleCookies、HTTPShouldUsePipelining、HTTPShouldUsePipelining、networkServiceType、timeoutInterval,这几个参数同时也是NSMutableURLRequest的成员变量的对应key值,通过这种方式实现了动态的参数设定:即设置当前Serializer会自动设置其Request参数。

1、请求参数设定

针对的都是GET、DELETE、HEAD请求,会将传入的NSArray、NSDictionary、NSSet parameters做格式化为URL参数,将PUT、POST、PATCH encode在body中。在对NSArray、NSDictionary、NSSet格式的参数进行处理时,首先会使用向外暴漏block进行拼接,block生成的string在url参数中会直接加载url后,body根据传入的encodeCode进行字符串编码。

2、POST文件上传

仅POST请求可以进行文件上传,content-type为multipart/form-data。本类中一般的篇幅都是对文件上传进行处理的。

在对外使用接口上,针对文件上传会通过block抛回一个id对象,调用方在此block进行文件的绑定,包括文件data、fileName以及mime等,这种设计方案在好奇心日报的Android也进行了一次基本类似的实现,经实际使用,直观好用。

具体实现是定义了一个名为AFStreamingMultipartFormData 实现了 AFMultipartFormData protocol,负责回传给调用方加以使用。

针对其中通过NSDictionary这种方式传进来得参数,类中定义个一个名为AFQueryStringPair的对象,主要对参数进行相应的格式化,转成需要key-value形式,然后通过appendPartWithFormData:name:这个方法进行拼装。针对这层数据,再使用一个名叫AFHTTPBodyPart的对象进行第二次封装,这里会封装stringEncoding,本组数据的headers,数据与数据之间的boundary,其中的body(可能是data、inputStream、fileUrl等)和对应数据长度bodyContentLength。这里非常适合在学习HTTP 1.1协议适合进行学习或者查看

此处有两个习惯比较好:一是使用NSParametersAssert对参数进行非空检查,而是对bool值得返回函数进行了error的反馈,具体看代码片段:

1
- (BOOL)appendPartWithFileURL:(NSURL *)fileURL
                         name:(NSString *)name
                     fileName:(NSString *)fileName
                     mimeType:(NSString *)mimeType
                        error:(NSError * __autoreleasing *)error
{
    NSParameterAssert(fileURL);
    NSParameterAssert(name);
    NSParameterAssert(fileName);
    NSParameterAssert(mimeType);

    if (![fileURL isFileURL]) {
        NSDictionary *userInfo = @{NSLocalizedFailureReasonErrorKey: NSLocalizedStringFromTable(@"Expected URL to be a file URL", @"AFNetworking", nil)};
        if (error) {
            *error = [[NSError alloc] initWithDomain:AFURLRequestSerializationErrorDomain code:NSURLErrorBadURL userInfo:userInfo];
        }
        return NO;
    }
    ....
  }

上面的最后会拼在一个NSInputSteam的子类AFMultipartBodyStream中,该类最后会进行流的最终拼接和上传。

上传中可以设置上传的大小以及每个具体子资源写入的延迟,用于在3G和EDGE下选择使用。(文档这么讲,不懂,以后了解)

AFN源码分析-开篇

开篇准备&目录分析

学AFNetWorking2.0很有必要先看业界的这个入门架构分析,本部分基本就是对该文章的二次描述。

旧版核心 — NSURLConnection + NSOperation

AFN在2.0时代有两类网络加载方案,一套是传承下来的NSURLConnection+NSOperation方案。

其中NSURLConnection是负责网络请求,异步地加载一个NSURLRequest对象,调用 delegate进行相关的回调处理;NSOperation是抽象类,模拟单个计算单元,有状态、优先级、依赖等功能,可以取消。AFNetworking将两者结合在一起,可以从头到尾监视请求的状态,并储存请求、响应、响应数据等中间状态。

在整个设计中所有的网络请求通过一个AFNetWorking线程通过NSURLConnection发送,NSURLConnection会新启一个线程用CFSocket去做网络请求,成功后的数据会回调在AFNetWorking线程,再抛回到UI线程的。

新版新增 — NSURLSession

NSURLSession是iOS7时代开始一个用于网络请求、上传、下载的抽象组件。

AFNetWorking中定义了一个AFURLSessionManager,以用于创建、管理基于 NSURLSessionConfiguration 对象的 NSURLSession 对象的类,也可以管理 session 的数据、下载/上传任务,实现 session 和其相关联的任务的 delegate 方法。

目录分析:

  1. UIKit文件夹:对一些系统组件的扩展,最终的应用级的东西
  2. Security:名如其意。文件夹只有一个类,主要用于关于HTTPS认证相关的工具类
  3. Reachability:文件夹只有一个类,用于网络环境检查以及相关切换、广播等
  4. Serialization:两个类,分别用于请求数据、HTTP请求header以及一些请求操作的封装、序列化操作,以及对response数据的反序列化。通过这两个类,将网络请求的in和out部分完全抽象出来,有利于进一步的定制化和扩展。
  5. NSURLConnection:根据NSURLConnection组成的网络请求封装方案
  6. NSURLSession:根据NSURLSession组成的网络请求封装方案

预计研究顺序

秉承着个人一贯的研究框架的源码顺序:先若干基础工具类—>再从调用类(一般叫manager)开始进行结构分支梳理—>manager下面具体管理的各个分支类—>具体应用层次的封装。根据上述过程,将源码研究分成几个步骤,由于NSURLConnection + NSOperation方案的研究价值更大一些,所以会先从这里入手:

  1. Security+Reachability文件夹,主要用于了解功能和熟悉API。
  2. AFHTTPRequestOperationManager,从manager入手,先了解比较泛的调用框架
  3. Serialization文件夹,基本和上面的manager结合了解大体结构和使用
  4. AFHTTPRequestOperation & AFURLConnectionOperation,方案1的核心部分,这里结合了NSURLConnection和NSOperation,基本都是具体实现和最终使用中一些不太常用的小技巧,多线程学习一块比较推荐的部分
  5. NSURLSession文件夹,该了解方案2了,这里就比较孤立了,因为准备工作都在方案1研究中完成了
  6. UIKit文件夹,了解下都有什么现成的封装,都用了哪些比较优秀个特性。

参考

《Effective Objective-C 2.0》读书笔记3---tips

读书笔记第三篇,主要就是学习总结书中的一些小知识点,这些知识点对于学习理解iOS系统有一定的帮助,不过若以后转向swift可能没有太多用处。
此处文章先hold住,慢慢补充

多用块枚举,少用for循环。
可以使用__bridge方式在Foundation框架中得Objective-C对象和CoreFoundation中得C数据结构进行转换。
使用缓存用NSCache不用NSDictionary,因为其会自己维护一个缓存淘汰算法,淘汰最久未使用的对象。而且NSCache是线程安全的。这里有个NSPurgeableData类型比较有意思,是NSMutableData的子类,和NSCache联合使用可以在NSPurgeableData内存释放时候自动清除缓存。
load方法:对于运行期系统的每个类(class)及分类(category),都必调用且仅调用一次。

基本调用顺序就是,执行当前类的load方法,必定先调用super的load方法,如果代码依赖其他程序库,那么程序库中得相关类的load方法也会先执行。类load方法执行完成后,会执行相应的分类的load的方法。
但是如果在一个程序库中得若干个class,却没法确定load顺序,所以load方法不建议使用其他class。
load方法在调用的时候,app会阻塞,所以一定要轻!
应用程序会阻塞并把所有类的load方法都执行完,才能继续。

load方法直接使用函数内存地址的方式(*load_method)(cls, SEL_load)调用的,而不是使用发送消息 objc_msgSend 的方式。
因此:load方法不遵循继承规则:如果一个class本身没有实现load方法,那么无论其各级super是否实现load,都不会调用;
同时:当一个类和它的分类都实现了 +load 方法时,两个方法都会被调用。
所以:像method swizzling这种操作会放在load里面进行。

load方法调用时,程序甚至没有autoreleasepool

initialize方法:首次使用该class前调用,且只有1次。

和load方法相比,他是“lazy”调用的,也就是类或它的子类收到第一条消息之前被调用的,这里所指的消息包括实例方法和类方法的调用。。
可以安全使用任何class,因为它们都已经load过了。
initialize方法执行一定在thread-safe environment,也就是说会阻塞所有线程。
initialize会走和objc_msgSend一样的消息发送原则,也就是会有覆盖。
实际编译器调用时,父类的会优先于子类调用。

相关load和initialize比较了解参考了以下博文:
Objective-C +load vs +initialize
Objective-C类初始化:load与initialize
iOS初探+load和+initialize

NSTimer的一些注意点

  • NSTimer一定要加入到runloop才有意义,如果没有addrunloop,那就加入到当前线程的runloop中,而子线程默认runloop没有run起来。
  • NSTimer的执行会持有target对象(因为一定是要target在一定时间调用其selector),这种持有会保持到失效为止,所以一定要注意针对repeating timer的循环引用问题。
  • 针对可能的循环引用问题,可以为NSTimer增加category,对实现增加block的实现方式。在调用时候只要注意一些block的内存使用问题就好了。

《Effective Objective-C 2.0》读书笔记1---消息发送

objc在函数调用采用的是发送消息的方式,使得调用方法在编译器不能确定,需要在运行时动态绑定,也就是所谓的runtime特性,可以这么说,objc的对象结构和其消息发送机制,决定了它的runtime特性。

一、Objective-C对象结构体

如下是一个具体实例对象的结构,其中有一个isa指针,指向其对应的Class在堆内存的地址,每个Class在内存中保存一个单例,该isa正是指向这个单例。

typedef struct objc_object {
    Class isa;
} *id;

该类isa指针指向的Class对象(类对象)也定义在运行期程序库的头文件中:

typedef struct objc_class *Class;
struct objc_class {
    Class isa;
    Class super_class;
    const char *name;
    struct objc_ivar_list *ivars;
    struct objc_method_list **methodLists;
    struct objc_cache *cache;
    struct objc_protocol_list *protocols;
} *id;

这个Class结构体就存储着类的“元数据”,存在isa指针说明这个也是一个OC对象(判断OC对象的方法就是isa指针),该指针指向该类的metaclass,表述类对象本身所具备的元数据。

插播Meta Class

可以把meta class理解为一个Class对象的Class。简单的说:

  • 当我们发送一个消息给一个NSObject对象时,这条消息会在对象的类的方法列表里查找
  • 当我们发送一个消息给一个类时,这条消息会在类的Meta Class的方法列表里查找

meta class的isa指针指向NSObject的meta class。

一个class的super_class指针指向它父类的class单例地址。

meta class的super_class指针指向原class父类的isa指针指向的meta class。

以此向上查找,一直找到NSObject(Class) 和 Meta Class of NSObject,NSObject的super class指向nil,meta class的super class指向NSObject,meta class的isa指针指向自己,实现了逻辑上的完善。具体关于isa指针的指向如下图所示:

再插播一条关于Class内省方法

isa指针的一个典型应用就是用于oc对象的内省方法isKindOf和isMemberOf

isKindOf就是比较目标类isa指针或者其父类
isa指针是否和当前类一致:

- (BOOL)isKindOf:aClass
{
    Class cls;
    for (cls = isa; cls; cls = cls->superclass) 
        if (cls == (Class)aClass)
            return YES;
    return NO;
}

isMemberOf更为粗暴,只比较当前类的isa指针是否一直即可:

- (BOOL)isMemberOf:aClass
{
    return isa == (Class)aClass;
}

指向保存Class成员变量的ivars指针

objc_ivar_list结构体存储着objc_ivar数组列表,也就是各个成员变量,Class中保存着该结构的指针。

指向Class中方法列表指针的methodList指针

class中得methodList是一个指向指针的指针,所以category才能在已有的类中添加方法,才能通过swizzling动态交换方法,可以说这个methodList是oc中消息发送、动态绑定的关键。具体使用会在下文的“传递消息”介绍。

指向保存method查询历史的cache指针

这个数组列表主要用于优化消息动态查找使用的,因为方法调用实在运行期动态绑定的,相对编译器确定的语言,不可避免会降低调用效率,增加调用时间,该数组就是优化该使用的。具体使用会在下文的“传递消息”介绍。

指向保存Class实现的protocol的protocols指针

二、传递消息

核心就是 objc_msgSend方法:

void objc_msgSend(id self, SEL cmd, ...)

在我们调用一个函数

id returnValue = [someObj msgName:parameter]

编译器会将其转化为如下函数

id returnValue = objc_msgSend(someObj, @selector(msgName), parameter);

然后编译器会寻找someObj中isa指针指向的Class中methodList指向的那个数组,如果找到相符的代码,则实现,没有找到,就在它的继承体系中继续寻找(找super_class指向的Class的methodList),一直找到NSObject,如果还没有找到,就执行消息转发(下文描述)。

按照上面介绍的查找路径,每个方法在处理过程中会经过比较多的步骤,为优化查找顺序,系统会将已经匹配的IMP缓存到快速映射表里(上文的cache)。

class的methodList会把selector的名称映射到实现方法里,系统的方法会比较靠前,自定义的(category等)方法会在后面,查找过程为从后往前查找。methodList中得这些实现方法均以函数指针的形式表示,即IMP:

id (*IMP)(id, SEL, ...)

在OC中,我们可以动态替换方法(method swizzling),就是通过methodList中得这种映射特性,将原来selector的名称映射的IMP和新的做交换,达到替换的目的。一般新建一个方法并替换系统方法的步骤为:

Method originalMethod = class_getInstanceMethod([UIView class], @selector(setFrame:));
Method swappedMethod = class_getInstanceMethod([UIView class], @selector(setNewFrame:));
method_exchangeImplementations(originalMethod, swappedMethod);

整理一下消息传递的流程

  1. 检查selector是否需要忽略。(ps: Mac开发中开启GC就会忽略retain,release方法。)
  2. 检查target是否为nil。如果为nil,直接cleanup,然后return。(这就是我们可以向nil发送消息的原因。)
  3. 然后在target的Class中根据Selector去找IMP

具体寻找IMP的过程:

  1. 先从当前class的cache方法列表(cache methodLists)里去找
  2. 找到了,跳到对应函数实现
  3. 没找到,就从class的方法列表(methodLists)里找
  4. 还找不到,就到super class的方法列表里找,直到找到基类(NSObject)为止
  5. 最后再找不到,就会进入动态方法解析和消息转发的机制。(第三部分介绍)

category同名方法的调用问题

我们是可以使用category定义和原Class中得相同方法,这些method会添加到Class的methodList数组列表底部,消息传递过程中,会从后查找,相当于覆写了系统方法。

如果两个category定义了同名的方法,这些方法也都会添加到methodList中,添加顺序为Build Phases中Compiles Sources中的文件顺序,后增加进去的会被调用,相当于覆写。

三、消息转发

消息转发发生在上面第二步一直到查找到NSObject得methodList依然没找到对应IMP的情况以后,在我们一般收到“unrecognized selector sent to instance 0x97”这种异常之前。这个异常是NSObject的“doesNotRecognizeSelector:”方法抛出的。

(1)先教科书下消息转发过程

  1. 动态方法解析(Dynamic Method Resolution或Lazy method resolution)
    向当前类(Class)发送resolveInstanceMethod:(对于类方法则为resolveClassMethod:)消息,如果返回YES,则系统认为请求的方法已经加入到了,则会重新发送消息。
  2. 快速转发路径(Fast forwarding path)
    若果当前target实现了forwardingTargetForSelector:方法,则调用此方法。如果此方法返回除nil和self的其他对象,则向返回对象重新发送消息。
  3. 慢速转发路径(Normal forwarding path)
    首先runtime发送methodSignatureForSelector:消息查看Selector对应的方法签名,即参数与返回值的类型信息。如果有方法签名返回,runtime则根据方法签名创建描述该消息的NSInvocation,向当前对象发送forwardInvocation:消息,以创建的NSInvocation对象作为参数;若methodSignatureForSelector:无方法签名返回,则向当前对象发送doesNotRecognizeSelector:消息,程序抛出异常退出。

所有相关的方法包括:

方案一:
+ (BOOL)resolveInstanceMethod:(SEL)sel
+ (BOOL)resolveClassMethod:(SEL)sel 

如方法可见,方案一的懒加载为静态方法,就是用于对类进行动态添加对应IMP,不算是真正意义上的转发消息,只是进行未找到selector的第一个解决办法,会直接修改类对象,在所有的该类的子类都会进行修改。一般针对@dynamic标记的变量进行动态添加get和set方法在这里进行处理。

方案二:
- (id)forwardingTargetForSelector:(SEL)aSelector

方案二多数是快速转发,将消息原封不动的转发给另一个对象,一般用于实现伪多继承。

方案三:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;

如果方案一和二都没走通,就会走一个完整的消息转发流程。此处会创建一个NSInvocation对象,里面包含target和selector以及所有的参数和返回值。

由于构建NSInvocation会先需要一个NSMethodSignature,这个签名是描述这个selector的返回值和参数的,完整的消息转发必须生成签名,如果签名为nil就会出现“unrecognized selector sent to instance”的错误。

在forwardInvocation中拿到NSInvocation对象后,可以随意修改其参数、selector或者对象。

完整的转发流程如下所示:

(2)NSProxy

消息转发中得快速转发一个比较大得你应用场景就是多代理和多继承。

Objective-C中有两个根类,一个是NSObject,另一个就是这个NSProxy。

参考:

http://chun.tips/blog/2014/11/06/bao-gen-wen-di-objective[nil]c-runtime(3)[nil]-xiao-xi-he-category/

http://blog.csdn.net/yanghua_kobe/article/details/8395535

http://tutuge.me/2015/02/16/利用NSProxy实现消息转发-模块化的网络接口层设计-原创/