罗晨汛

罗晨汛

移动互联网开发者

iPhone 设备虽然还没有像 Android 设备那样碎片化,但是随着时光的流淌,iPhone 设备也越来越多,先是 plus 机型打破了 iPhone 一手掌握的定律,再是 “刘海屏”。本文旨在记录 iPhone 设备屏幕适配那些事,持续更新。。。

1 iPhone 设备分辨率大全

2 机型适配技术

2.1 判断全面屏设备 (带刘海)

iPhoneX、iPhone XR、iPhone Xs、iPhone Xs Max 为 iPhone 新一代全面屏设备,这几款机型的相似点在于:

  • 机型的上部有刘海设计;
  • 屏幕下方有一段安全距离 (软 home 键)

可以通过判断底部是否有安全距离 (safeAreaInsets.bottom> 0.0) 来判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static inline BOOL isIPhoneXSeries() {
BOOL iPhoneXSeries = NO;
if (UIDevice.currentDevice.userInterfaceIdiom != UIUserInterfaceIdiomPhone) {
return iPhoneXSeries;
}

if (@available(iOS 11.0, *)) {
UIWindow *mainWindow = [[[UIApplication sharedApplication] delegate] window];
if (mainWindow.safeAreaInsets.bottom > 0.0) {
iPhoneXSeries = YES;
}
}

return iPhoneXSeries;
}

3 iPhone 设备指令集

iPhone 设备指令集
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
armv6
iPhone、iPhone 3G
iPod 1G、iPod 2G

armv7
iPhone 3GS、iPhone 4
iPod 3G、iPod 4G、iPod 5G
iPad、iPad 2、iPad 3、iPad Mini

armv7s
iPhone 5、iPhone 5C
iPad 4

arm64
iPhone 5s iPhone 6iPhone 6P iPhone 6s iPhone 6sP iPhone 7 iPhone 7P
iPad Air, Retina iPad Mini

4 引用

  1. paintcodeapp 感谢 PaintCode,贡献了非常牛逼的 iPhone 全机型分辨率图。
  2. kapeli 汇总了各种尺寸、分辨率等iPhone机型信息。

1 周期天王-周金涛

周金涛,中信建投首席经济学家,2007年成功预测次贷危机,2013年提出房地产周期拐点,2015年成功预测了全球资产价格动荡,并在2015年11月预言中国经济将于2016年一季度触底。因为其预测太过牛逼,人称“周期天王”。

他的名言有(本文下面的引用为他在演讲中的讲话或是网络中文章的原文):

40岁以上的人,人生第一次机会在2008年,第二次机会在2019年,最后一次在2030年附近,能够抓住一次你就能够成为中产阶级;

1985年之后出生、现在30岁以下的人,第一次人生机会只能在2019年出现。

周金涛有一句名言叫“人生发财靠康波”,意思是,每个人的财富积累,一定不要以为是你多有本事,财富积累完全来源于经济周期运动的时间给你的机会
在他看来,一个人的一生中所能够获得的机会,理论上来讲只有三次,如果每一个机会都没抓到,你肯定一生的财富就没有了。如果抓住其中一个机会,你就能够至少是个中产阶级。

2 什么是康波理论

2.1 康德拉基耶夫长波周期

所谓康波理论,其实是康德拉基耶夫长波周期。康德拉季耶夫(Nikolai DimitrievichKondratiev,1892—1941)是十月革命前后都很活跃的俄国学者,作为国际知名的经济学家,他的声誉主要来自长波理论。所谓“长波”指的是经济成长过程中上升与衰退交替出现的一种周期性波动。由于康德拉季耶夫观察到的周期比人们观察到的另外两种经济波动的周期“尤格拉周期”和“基钦周期”明显要长,所以被叫做长波或者长周期。

在康德拉季耶夫以前,人们已经注意到经济发展过程中长时段的繁荣与萧条的交替存在着某种规律性。第一次世界大战期间,一些经济学家已经提出过长周期的设想。康德拉季耶夫的贡献在于用大量经验统计数据检验了长周期的设想,从而使之成为了一种比较系统的周期理论。因此,1939年经由熊彼特提议,世界经济学界都接受了用“康德拉季耶夫周期”这一术语指称经济成长过程中长时段的波动。

他分析了英、法、美、德以及世界经济的大量统计数据,发现发达商品经济中存在着为期54年的周期性波动。在50年左右的周期中,一般说头15年是衰退期;接着20年是大量再投资期,在此期间新技术不断采用,经济发展快,显示出一派兴旺景象;其后10年是过度建设期,过度建设的结果是5~10年的混乱期,从而导致下一次大衰退的出现。熊彼特等人后来继承和发展了长波理论,并重新确定了资本主义经济三次长周期的起止时间。

康德拉基耶夫长波周期
1
2
3
0 ------ 15 ----------- 35 ------  45 ----- 50 --
| 头15年 | 中间20年 | 其后10年 | 最后5-10年
| 衰退期 | 再投资期(兴旺)| 过度建设 | 混乱期

2.2 熊彼特长波理论

熊彼特在1934年英译版的《经济发展理论》中对三次长周期的分期为:

(1) “长波”I —— 从大约1783年到1842年,是所谓“产业革命时期”,这个周期的基本特征是手工制造或工场制造的蒸汽机逐步推广到一切工业部门和工业国家。
(2) “长波”II —— 从1842年到1897年,是所谓“蒸汽和钢铁时代”或“铁路化时代”,其特征是机器制造的蒸汽机成为主要的动力机,并得到普及。
(3) “长波”III —— 从1897年开始(当时这个“长波”尚未最后结束),是所谓“电气、化学和汽车时代”,其特征是电动机和内燃机在一切工业部门中的普遍应用

2.3 长波理论未能被论证

不过,虽然康德拉季耶夫猜测长波的存在与科技革命浪潮有关,但对长波出现的原因却没有给出令人信服的解释,特别是20世纪80年代以来,使用新数据和新技巧的计量经济学研究无法确定长波是否真的存在,这使长波理论带有显著的经验假说性质。

非常有意思的是,世界上有许多人试图解释长周期存在的原因,提出的猜想五花八门,无奇不有。

例如,有人认为长周期的存在和太阳黑子活动的周期有关,有人则认为跟人的世代交替有关,还有人认为跟厂房和设备的更新周期有关,人们还可以猜想长周期跟专利保护的年限有关,从理论上讲,宏观范围内的任何周期都对长周期有或多或少的影响。熊彼特曾试图用他的创新理论对长周期给出了一个系统的解释,但创新理论无法解释为什么一个周期是55年而不是75年。

3 周金涛的康波理论

周金涛实际上对康波理论进行了自己的修改,提出了其所谓“康波理论”。他将康德拉季耶夫周期修改为60年,将阶段总结为:回升、繁荣、衰退、萧条。

在世界经济周期运动中最长的周期是康德拉季耶夫周期,它一个循环是60年一次。大家知道60年就是一个人的自然寿命是60年,中国讲60甲子,循环一次就是一个康德拉季耶夫周期。它分为回升、繁荣、衰退、萧条。

3.1 金源康波

金源康波
1
2
3
4
1991 ---  1994 --- 2002 | 2004 ----- 2015             |  15 - 19年迎来萧条
1 ----- 4 ----- 10 | 12 -------- 25 |
美国信息技术泡沫 | 经济增长放缓,但是还是向上趋势 |
(繁荣) 泡沫过后7年增长 | (衰退) | (萧条)

我看去年大家讨论资产荒,资产荒就是我钱多,但是没有收益率,这是很危险的信号,这是一个转折点,转折点意味着未来四到五年的总体的资产收益率不仅不赚钱,甚至可能要亏损。

2015年之后,全球应该进入的康波的萧条阶段,在康波的萧条来临之前,会发生哪个现象?我们觉得手中有很多流动性,大家这个流动性过去6年还通过炒股票赚到钱,大家2015年开始不赚钱。

怎么办?我需要为我手中的钱保值,所以,大家想了一个办法就是买进一线城市核心区域的房地产。第二个办法搞点新兴产业,很多人投了很多新三板。从我的本心出发,这两个办法都是消灭中产阶级财富的方法。

4 如何运用康波理论

4.1 在衰退期兑现

大家追逐的新兴产业,因为我是研究长波理论的,我看的很清楚,实际上所谓的互联网+,就是本次康波的技术创新,信息技术的最后成熟阶段。

大家知道,信息技术爆发期在80年代,90年代在美国主导国的展开期。当技术从主导国传导到中国,中国是本轮康波中的追赶人,传导到中国扩散到生活的每一个角落,那么你们想这个技术还有什么前途可言?

一个技术当它在追赶国的渗透到达了无孔不入的时候,一定到达了它生命周期的最后阶段,这个技术后面就是一个成熟并衰落的趋势。

所以,去年我跟朋友说,2016年和2017年是新三板的兑现阶段,能不能兑现要看你的命运了,这个东西不是我能够左右的。大家不要认为一定要兑现。

当大家看到这两个现象的时候,意味着康波要进入萧条期,你突然会发现,在经历一次滞胀之后,2016年到2017年是一次滞胀,一次滞胀发现你手中持有的流动性差的资产可能就没人要了。

在这个阶段之后,将进入货币消灭机制,就是这些资产的价格将下跌,这个就是康波理论告诉我们的。

4.2 规划人生

在金涛看来,人生的机会基本上由康波的运动给予的。

十年前你在中信建投证券找了一份工作不是太重要,大家十年前在中信建投证券旁边买一套房子真的很重要,因为中信建投在北京朝阳门,现在房子涨十倍,大家挣十年也挣不到。人生的财富不是靠工资,而是靠你对于资产价格的投资。

4.3 2类人生资产

一个人的人生财富有哪些?理论上只能有这么几类:第一类就是大宗商品,这个实际上大家不要小看,在60年巡回中,大宗商品是最暴利的行业。就是刚才讲的,中国人觉得最爆发的煤老板,原因就在此。

第二类资产是房地产,就是经济周期理论中的枯枿周期,房地产周期20年轮回一次,一个人当中可以碰到两次房地产周期。为什么呢?人的一生作为群体来讲会两次买房,第一次是结婚的时候,平均27岁,第二次是二次置业,改善性需求是42岁左右。一个人的消费高峰,最高峰出现在46岁的时候,46岁之后这个人的消费就往下走,你的消费逐渐由房子这些变成医疗养老。

根据他的计算,中国本轮的房产1999年开始,经历了一个20年周期。

中国房产走势
1
2
1999 -- 2007 | 2009 - 2014 | 2014 - 2020
回升 | 高点 | 萧条

他推测2014年是高点,然后说明了为什么从2014年是开始走下坡路,虽然这时一线城市房价还是涨的。

但是,到2015年的时候大家突然发现,房子又好卖了,2016年一线城市核心区域房地产暴涨,但这不是房地产重新开始,而是波浪。三四线城市的房子能涨才是牛市,垃圾股不涨,一小撮股票涨不是牛市,这是房地产周期的波浪反弹

5 引用

  1. 康波周期理论简介 百家号上找到的介绍文章

在web程序中进行url请求时,常会遇到url中含有特殊字符的问题(utf8或gbk),常见的特殊字符有 ?$&*@等字符,或者是中文。遇到这种情况时,就要对url进行编码,用一种规则替换掉这些特殊字符。

阅读全文 »

公司的项目使用了 ReactNative 0.50,最近要抓 app 稳定性,组里开启了清 Bug 大行动。行动过程中发现了一处 RN 的 Bug,觉得问题还比较典型,下面我们一起来看看。

1 初步探寻

Bugly 上报的日志如下:

Bugly 日志
1
2
3
4
5
6
7
8
9
libicucore.A.dylib uregex_setUText + 36
Foundation -[NSRegularExpression(NSMatching) enumerateMatchesInString:options:range:usingBlock:] + 480
Foundation -[NSRegularExpression(NSMatching) enumerateMatchesInString:options:range:usingBlock:] + 480
Foundation -[NSRegularExpression(NSMatching) firstMatchInString:options:range:] + 128
MyApp -[RCTImageLoader canHandleRequest:] + 304
MyApp -[RCTNetworking handlerForRequest:] + 396
MyApp -[RCTNetworking networkTaskWithRequest:completionBlock:] + 72
MyApp -[RCTNetworking sendRequest:responseType:incrementalUpdates:responseSender:] + 676
MyApp __44-[RCTNetworking sendRequest:responseSender:]_block_invoke + 220

这里可以看到,Bug 发生在 ReactNative 源码的 RCTImageLoadercanHandleRequest 方法中。我们看看这个有问题的方法 canHandleRequest

出现问题的 RCTImageLoader 源码
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
- (BOOL)canHandleRequest:(NSURLRequest *)request
{
NSURL *requestURL = request.URL;

// If the data being loaded is a video, return NO
// Even better may be to implement this on the RCTImageURLLoader that would try to load it,
// but we'd have to run the logic both in RCTPhotoLibraryImageLoader and
// RCTAssetsLibraryRequestHandler. Once we drop iOS7 though, we'd drop
// RCTAssetsLibraryRequestHandler and can move it there.
static NSRegularExpression *videoRegex = nil;
if (!videoRegex) {
NSError *error = nil;
videoRegex = [NSRegularExpression regularExpressionWithPattern:@"(?:&|^)ext=MOV(?:&|$)"
options:NSRegularExpressionCaseInsensitive
error:&error];
if (error) {
RCTLogError(@"%@", error);
}
}

NSString *query = requestURL.query;

if (query != nil && [videoRegex firstMatchInString:query
options:0
range:NSMakeRange(0, query.length)]) {
return NO;
}

for (id<RCTImageURLLoader> loader in _loaders) {
// Don't use RCTImageURLLoader protocol for modules that already conform to
// RCTURLRequestHandler as it's inefficient to decode an image and then
// convert it back into data
if (![loader conformsToProtocol:@protocol(RCTURLRequestHandler)] &&
[loader canLoadImageURL:requestURL]) {
return YES;
}
}
return NO;
}

从注释中我们了解到这个方法主要是对 request 进行过滤,使用了一个正则 videoRegex 去对传入的 request 进行匹配。根据 (?:&|^) ext=MOV (?:&|$) 规则,去判断如果是视频文件就返回 NO,从而跳过处理 (因为这个是 ImageLoader 嘛就不处理视频)。

2 分析

我们先看看出问题的一句,我们从 Bugly 日志中问题发生的下个入口 (通常问题发生在从 MyApp 转到 Native 处) 初步认定问题发生在 canHandleRequest 的第 23 行 firstMatchInString 处。

1
2
3
4
5
6
7
NSString *query = requestURL.query;

if (query != nil && [videoRegex firstMatchInString:query
options:0
range:NSMakeRange(0, query.length)]) {
return NO;
}

对于这个 firstMatchInString 我们从出错堆栈中看出这个方法里面使用 enumerateMatchesInString:options:range:usingBlock: 去遍历字符串,在代码示例中的 if 语句里,query 这个字符串不可能为空,因为之前使用 query != nil 判断过了。query 的类型为 NSString,是不可变字符串,代入此方法应该也不会出问题。那么我们问题有可能出自 videoRegex

2.1 发现问题

videoRegex 是一个静态成员变量,我们知道,oc 的静态成员变量只会生成一份内存,这里 RN 之所以使用静态成员变量是因为这个正则的匹配器每次都是一样的,所以只需要初始化一次,后面每次都使用同一个匹配器就好了。但是,这里隐藏了一个常见的并发问题。

我们在学习 OC 的单例模式时常学习这个,使用静态成员变量时常出现这样的并发问题,我们细看这里的源码:

1
2
3
4
static NSRegularExpression *videoRegex = nil;
if (!videoRegex) {
videoRegex = initMethod;
}

当多线程访问此方法时,线程 A 跑到 if (!videoRegex) { 这一句时,判断 videoRegex 为空,就进入方法块对它进行初始化。如果这时刚好发生线程切换,此时线程 B 也跑到这个 if 判断来,因为 videoRegex 还没初始化完成,所以其仍然是空,因此线程 B 也开始进入方法块,对之进行初始化。

这样问题就发生了 – 这个静态变量初始化了 2 次!

这会导致什么问题呢?如果线程 A 初始化完之后,进入下面的正则匹配,从 firstMatchInString 跑到 enumerateMatchesInString:options:range:usingBlock: 去遍历匹配字符串。但是这期间线程 B 将匹配器 (videoRegex) 重新初始化了!我们不希望发生的事情 (crash) 就这样发生了!

2.2 构造场景复现问题

当我们发现疑似问题时,特别是这种难以复现的问题,有一个重要的步骤就是构造场景复现问题。因为我们的猜测有可能是错的,如果猜错了盲目进行修复,很可能上线了发现问题并没有真正修复,而且就算修复了我们心里也不会太有底,不知道问题是不是正的就修复了。

因为我们怀疑可能是并发问题,于是就构造多个线程并发访问,从而构造了一个复现场景,如下:

构造场景复现问题
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
- (void)testCode
{
for (int i = 0; i <= 1000; i++) { // 构造 1000 个线程并发访问有问题的代码
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
if (i % 4 == 0) { // 这里构造了几查询字符串用以判断 Crash 和查询字符串有没有关系
[self bugCodeWithInput:nil];
} else if ( i % 4 == 1) {
[self bugCodeWithInput:@"https://xx/xxx.MOV"];
} else if ( i % 4 == 2) {
[self bugCodeWithInput:@""];
} else {
[self bugCodeWithInput:@"https://xx/xx.png"];
}
});
}
}

- (void)bugCodeWithInput:(NSString *)query
{
// 怀疑有问题的代码
static NSRegularExpression *videoRegex = nil;
if (!videoRegex) {
NSError *error = nil;
videoRegex = [NSRegularExpression regularExpressionWithPattern:@"(?:&|^)ext=MOV(?:&|$)"
options:NSRegularExpressionCaseInsensitive
error:&error];
}

if (query != nil && [videoRegex firstMatchInString:query
options:0
range:NSMakeRange(0, query.length)]) {
}
}

运行 testCode,发现果不其然,crash 发生了!看来问题就是和我们分析的一样。下面我们来尝试解决之。

2.3 解决问题

解决这类问题其实很简单,就像常在单例模式里做的一样,使用 dispatch_once_tblock 就可以解决 (因为 dispatch_once 会根据传入的 token 只执行一次,从而避免了静态变量的多次初始化),我们将初始化正则匹配器的方法改成如下:

解决初始化静态变量的并发问题
1
2
3
4
5
6
7
8
9
10
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSError *error = nil;
videoRegex = [NSRegularExpression regularExpressionWithPattern:@"(?:&|^)ext=MOV(?:&|$)"
options:NSRegularExpressionCaseInsensitive
error:&error];
if (error) {
RCTLogError(@"%@", error);
}
});

我们修改上面 testCode 中 怀疑有问题的代码 这一段替换成上面解决的代码,再次运行我们构造的复现场景,发现 Crash 消失了!自此,videoRegex 就只会初始化一次,问题完美解决!

3 小结

这其实不是一个很难的 Crash,本文旨在提供一个解决问题的常见思路,特别是遇到这种难以复现的问题,构造问题的复现场景是一个非常好的办法,它可以帮我们确定我们的判断是否正解、解决方法是否正确。

还有一个就是平时多看代码多积累,比如这里的静态变量问题,看多了很容易联想到single模式中的用法,从而发现这里存在常见的并发问题。

Objective-C 的 “圣经” – 《CFHipsterRef》对 Objective-C runtime 有句经典的描述 (下文我们将 Objective-C 简称 OCObjective-C runtime 简称 Runtime):

Objective-C, by itself, is just talk. All of its high-falutin’ ideas about message passing and dynamism is nothing but a bunch of hot air without a runtime to do the hard work.

OC 语言的动态及消息传递的特性,都是基于 Runtime 实现的,Runtime 之于 OC 语言,就像一个小型的操作系统。而其中最本质的,OC 中定义的类、方法的实现、表达式等都会编译成 C 语言实现,然后再与 Runtime 交互。但是,这一切对一个 OC 程序员来说是透明的,一般情况下,我们无需知道这些底层的细节就可以运用 OC 的这些特性为我们服务。

但是,基于我们的 “三问精神”,我们希望了解这些底层实现细节,做到知其然也知其所以然。本章,我们就一起学习下 Runtime 中有关类结构与面向对象的实现相关技术。

1 概述

其实当我们说 面向对象编程 的时候,我们主要就是在说他的几大编程特性,或者也可以说是思想 (因为这几个特性太过著名,已经深入大家之灵魂了):

  1. 封装;
  2. 继承;
  3. 多态。

封装 是 OOP (下文中将面向对象编程都简称为 OOP) 中基础中的基础,其本质就是将我们抽象出来的客观事物使用一堆函数和变量或其它对象的引用表示出来。对外暴露出接口,而隐藏其实现细节的技术。通过本章的学习,我们可以发现 OC 其实就是使用 C++ 中的结构体 (struct) 来实现类、对象的结构。

继承 是 OOP 的另一个重要思想。通过继承,实现了对已有数据结构、数据抽象的良好复用,以及可以为我们项目建立优良的模型架构。我相信本文的读者大多都知道 OOP 中的 继承 是怎么一回事了,那么在 Runtime 中,类的继承关系是怎样实现的,子类通过怎样的方式访问父类的成员变量及方法的,这就是后面我们主要共同学习的内容。

多态 的特性其实是基于 继承 的,他使子类拥有相同的接口,使上层可以使用统一的调用,有了这个技术,他可以使我们的上层代码统一、简洁而优雅。本文中,我们将探究下 OC 多态实现相关的内容,实例方法是如何调用的,通过方法调用的原理,我们就可以体会到多态的实现。

总而言之,通过本章的学习,我们可以深入地了解 OC 中面向对象编程这套范式的实现方式,对加深 Runtime 的理解也是有所帮助滴~

1.1 准备工作

这里先推荐一个牛逼的 Runtime 源码项目:https://github.com/RetVal/objc-runtime

当然,学 iOS 开发的无人不知 objc4 源码,推荐这个项目的因为是他太牛逼了,也太无私了,哈哈哈。这位大神将 objc4 源码中所有依赖问题都解决了,然后还准备好了 Demo 项目供我们直接使用,可以为我们省很多事,强烈推荐。

我后面的研究都是基于他的这个库,在这里对他表示一下感谢与崇拜。

然后在开始之前我这里还是要多说几句

2 Runtime 中的类结构

面向对象思想的第一个有意思的思想就是 一切皆对象。众所周知,在面向对象的世界中,一切都是由对象组成的。

但是虽说一切皆对象,熟悉面向对象技术的同学都知道,实际上面向对象的世界是使用 来抽象的,而 对象 只不过是 的实例。因此,我们的分析就从 的定义开始。在 Runtime 源码中, 的定义如下:

Class 源码定义
1
typedef struct objc_class *Class;

这里可以知道的是,Class 可以用来定义一个 类结构,但是其不是关键字,其实是 objc_class 这个结构体的别名。那么我们通过分析 objc_class 这个结构体,就可以一窥其实现细节了!

这里在看 objc_class 之前,我们巩固下我们面向对象的世界观~在 OC 中,所有类都有一个共同的祖先就是 NSObject,这个大家都知道。我们不妨看下 NSObject 的定义,可以看出,他就是一个 – 其实就是一个 Class 结构体的实例。我们可以想像,程序启动后,会把我们定义的类加载到内存中,其中每个定义的类都会对应唯一一个 Class 结构体实例,而他们共同的祖先就是这个 NSObject 类结构体实例

1
2
3
4
5
6
7
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}

//... NSObject 方法列表及定义

@end

接下来我们具体看看 Runtime 中 objc_class 的定义。这里留意下,我专门罗列了 objc2.0 老版本和新版本中 objc_class 的定义,因为网上会有挺多讲 objc_class 的文章,很多用的是老的定义,可能会给大家带来很多困惑。但这里我更想大家体会一下的就是,其实版本的新旧问题不大,我们真正要做的是感受 OOP 设计与实现的原理及奥妙,因为思想的东西其实是很少会变的,所改动的地方无非是提高效率与优化而已。

我们先来看看老的定义:

objc2.0 之前的 class 定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct objc_class {
// 指针,表示是一个什么
// 实例的 isa 指向类对象,类对象的 isa 指向元类
Class isa OBJC_ISA_AVAILABILITY;

#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_list *protocols OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE; // OBJC2_UNAVAILABLE 与前面的 #if !__OBJC2__宏,说明在 objc2.0 中已不使用了

可以看出,老的 Class 结构是独立的,包括了 元类 的设计(这个后面会介绍),还有类结构相关的信息(类名、父类),成员变量列表、方法列表、协议、方法缓存等。

我们再对比看看新的 Class 定义。新的 objc_class 定义在源码 objc-runtime-new.h 中。

新 objc_class 定义 (objc-runtime-new.h)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct objc_class : objc_object {
// Class ISA;
Class superclass; // 父类
cache_t cache; // 缓存 key-IMP 映射,用于方法缓存与快速寻找
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags

class_rw_t *data() {
return bits.data();
}
void setData(class_rw_t *newData) {
bits.setData(newData);
}
// 省略其他方法
// 。。。
}

可以看到,objc_class 继承自 objc_object, 也就是说,在新一版的 objc4 实现上做了重新设计。即在 runtime 中,class 也被看做一种对象。这样做有什么好处呢?我能想到的就是这样使其 OOP 的实现设计更加统一了。因为在 OC 中 类 (Calss) 也具有一些对象一样的特性,比如 类方法,你可以给 发送消息调用其 类方法。而 类实例 在内存中也会像一个对象实例一样占有一份内存,这样对象和类的关系就有点像 JS 里面的 原型 (prototype),学完本文后感兴趣的童鞋可以去了解下~

而从上面类的结构中我们可以看出其中几个重要的信息:

  1. superclass 指针 (Class 就是 objc_class *,上文已经分析了哈),指向其父类,这就回答了我们关于 OC 类继承的实现的问题。类的继承和我们想像中的一样就是子类有指向其父类的指针。方便要访问父类成员或方法时可以引用。
  2. 第 2 点就是 OC 的一个特性。因为 OC 方法的调用采用的是消息传递的机制,而针对消息的方法查找需要一个过程。Runtime 就在类结构中加了一个这样的 缓存表,常命中的方法放到缓存中去,以便消息查找时先从缓存表中去查(这里涉及一个 28 理论,就是一个类常用的方法也就是 80% 会被调用的通常只是它所有方法中的 20%。)。
  3. class_data_bits_t,这个数据结构比较巧妙,它主要是一个指针,指向了类的主要封装的数据结构。我们下面就来主要看看这个。

在分析 class_data_bits_t 之前我们小看一下 cache_t cache;。我们说它其实是一个方法的缓存表。

cache_t 方法缓存表
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct cache_t {
struct bucket_t *_buckets;

//...
}

struct bucket_t {
private:
cache_key_t _key;
IMP _imp;
//...
}

typedef uintptr_t cache_key_t;
typedef unsigned long uintptr_t;

后面我们介绍消息转发机制时会详细介绍这几个数据结构。这里你只要了解下 IMP 就是具体方法实现。uintptr_t 这个数据结构 Runtime 中使用的较多,你可以将之看作为 void * 或一个引用。所以这里我将 cache 称为 key-IMP 映射,使用 key 对此缓存表查找可以快速地找到常用的方法。

下面我们就来看看 class_data_bits_t,前面说过了,它指向了类的主要封装的数据结构,也就是是类 封装 特性的主要实现。

2.1 类封装结构指针 class_data_bits_t

class_data_bits_t 是 Class 类结构的核心。它是一个复合指针,相应的位表示特定的信息,用不同的 flags 与与可以取位得到想要的数据。

class_data_bits_t 结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct class_data_bits_t {
// Values are the FAST_ flags above.
uintptr_t bits;

public:
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
void setData(class_rw_t *newData)
{
assert(!data() || (newData->flags & (RW_REALIZING | RW_FUTURE)));
uintptr_t newBits = (bits & ~FAST_DATA_MASK) | (uintptr_t)newData;
atomic_thread_fence(memory_order_release);
bits = newBits;
}
// ...
}

可以看出,class_data_bits_t 其实只有一个数据成员,就是 uintptr_t bits。上面我们分析过,uintptr_t 是一个 8 字节 (u long) 无符类型,存的一般就是指针。这个指针的低位一定是没用到,所以 Runtime 没有浪费,用来存类的相关信息了,要用的时候就用相应的 flag 与之与操作 (就像掩码一样),就可以取出相应数据。在 Runtime 中有许多这样的设计,这样做的好处就是减少了数据结构的存储空间,使结构体内的成员分布最精简,因为面向对象语言嘛,肯定会产生大量的类和对象结构。

比如说 bits 中使用一位来表示这个是 Swift 类还是 OC 类,我们用 FAST_IS_SWIFT 这个 flag 和 bits 做与操作,就可以取出对应的位:

通过 class_data_bits_t 与 flag 判断是 Swift 类还是 OC 类:
1
2
3
4
5
6
7
8
bool isSwift() {
return getBit(FAST_IS_SWIFT);
}

bool getBit(uintptr_t bit)
{
return bits & bit;
}

这样我们就理解了 data () 这个方法了,通过 FAST_DATA_MASK 取出的数据就是指向类封装数据的结构体指针,如下。

取 objc_class 类封装数据
1
2
3
class_rw_t* data() {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}

而这个指针的类型是 – class_rw_t。我们再看看 class_rw_t

2.2 类核心结构 class_rw_t

类封装数据核心结构 --class_rw_t
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
// 外层 Runtime 类结构
struct class_rw_t {
uint32_t flags;
uint32_t version;

const class_ro_t *ro; // 类只读基础信息

// 下面三个 array,method,property, protocol,可以被 runtime 扩展,如 Category
method_array_t methods; // 类方法列表
property_array_t properties; // 类属性列表
protocol_array_t protocols; // 类协议列表

// 和继承相关的东西
Class firstSubclass;
Class nextSiblingClass;

// Class 对应的 符号名称
char *demangledName;

// ...
}

// 类不可修改部分数据结构 (类基本信息)
// 存放类在编译期就确定的结构信息
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif

const uint8_t * ivarLayout;

const char * name; // 类名
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars; // 类

const uint8_t * weakIvarLayout;
property_list_t *baseProperties;

method_list_t *baseMethods() const {
return baseMethodList;
}
};

我们可以看到,类的封装数据无非就是我们之前设想的成员变量与方法列表等,但是它是封装成 2 层结构。外层是一些可扩展的成员列表 (类方法、类属性、类协议),内层是一个只读的 类基本信息。之所以这样做是因为 OC 是一个动态的语言,其方法与成员可以在运行时动态添加,而这个动态的特性就是这样实现的。比如当我们使用 Category 语法给一个类添加方法,会添加到外层的类可扩展的成员列表中,这样当进行消息查找时,就会先在外层查找,确保先找到我们动态添加的特性。

至此,对类的分析就基本结束了。

做个小结,我们知道了 OC封装 这个特性是靠类来实现的,类在 Runtime 中是 objc_class 结构体。其核心成员 class_data_bits_t 是一个复合指针,指向类核心封装数据结构 class_rw_t。而 class_rw_t 采用了 2 层设计,外层存放了一些可扩展的成员列表。内层 class_ro_t 是类的只读基本信息。

下面我们看看 Runtime 中对象的实现。

3 Runtime 中对象的实现

Runtime 中,对象被定义为 objc_object 结构体。我们在上面的介绍中已经了解到,类 (objc_class) 是继承自对象结构体 (objc_object) 的,因此,类可以看做是一类特殊的对象。

Runtim 中对象结构体的定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct objc_object {
private:
isa_t isa;

public:

// ISA() assumes this is NOT a tagged pointer object
Class ISA();

// getIsa() allows this to be a tagged pointer object
Class getIsa();

// ...
}

可以看到, objc_object 的定义很简单,仅包含一个 isa_t 类型。

3.1 isa_t

我们通过分析这个 isa_t 类型,可以发现这个属性和类的 class_data_bits_t 成员有点相像,都是使用了一个指针结构但是复用了指针内的一些位来表示一些信息或标志位。印象不够深的同学可以回上去复看一下 class_data_bits_t。我寻思这种设计的初衷就是因为 Runtime 中许多内存使用是对齐的,因此一些特定结构的地址指针的低位就没有用,为了节约,Runtime 就往这些没用到的低位字节中 “塞” 了一些数据,要用的时候使用 flag 掩码按位与取出来用即可

我们下面看看这个 isa_t

objc_object 中的 isa_t 联合体
1
2
3
4
5
6
7
8
9
10
11
union isa_t
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }

Class cls;
uintptr_t bits;

// 省略其余
。。。
}

isa_t 是一个联合,可以表示 Class cls 或 uintptr_t bits 类型。实际上在 OC 2.0 里面,多数时间用的是 uintptr_t bits,因为 64 位系统中,这个指针结构可以额外存很多信息 (原因上面说过了)。要详细了解 isa_t,可以学习下这篇文章:[《从 NSObject 的初始化了解 isa》 - Draveness](https://github.com/Draveness/analyze/blob/master/contents/objc/ 从 %20NSObject%20 的初始化了解 %20isa.md), Draveness 的这篇文章详细地分析了这个数据结构中表示的对象信息。

我们现在只要知道的是 isa_t 中的 bits 其实主要也是 Class 指针。另外附带一些对象的特定信息,如:

  • has_assoc:对象含有或者曾经含有关联引用,没有关联引用的可以更快地释放内存
  • weakly_referenced: 对象被指向或者曾经指向一个 ARC 的弱变量,没有弱引用的对象可以更快释放
  • has_sidetable_rc: 对象的引用计数太大了,存不下
  • deallocating: 对象正在释放内存

所以现在我们知道了,objc_object 中的这个唯一的成员 isa_t 其实也是一个指针,指向一个 Class 类结构。而这个 Class,就是我们要引入的一个新概念 – 元类 (meta class)

3.2 元类与消息查找机制

在 Objective-C 中,对象的方法并没有存储于对象的结构体中(实际上在任何一种面向对象语言的实现中,实例方法都不会放到对象结构中。因为对于同一个类的多个实例对象来说,同一个方法的实现都是一样的,没必要重复放在对象结构中从而浪费内存)。

根据本章上面的介绍我们都知道,方法们都封装在类结构体 (objc_class) 中。那么我们给实例发消息时,是怎样调用到类方法列表中的这些方法呢?

下面说一点本章的 “超纲内容”,在后面的 《深入 Runtime 之消息机制》 中有更细的介绍:

在 OC 中,要调用一个对象的方法,得像这样给它发消息:

在 objc 中给对象发消息语法
1
id returnValue = [object messageWithParam:param]

Runtime 会将像上面的消息语法转成下面这样:

objc 发送消息的实质
1
2
3
4
id objc_msgSend (id self, SEL op, ...);  // 后面的... 为可变参数,表示接受 2 个或以上参数

// 比如示例 1 中的 oc 语句将转为如下
id returnValue = objc_msgSend(object, @selector(messageWithParam:), param);

其中,我们在 OC 中调用的方法名也就是发送的消息名,我们需要先把它使用 @selector (messageWithParam:) 编码成一个 SEL 变量,我们称之为 选择器。这个 选择器 就是我们之前说 key-IMP 映射时的 key,通过这个 key 来找到真正的方法实现 IMP
而 msgSend 的内部就是根据这个 选择器 到类内部的 方法列表 中去查找,如果找不到将经过一个我们称之为 消息转发 的过程 (后面会介绍 《消息转发机制》)。

objc_msgSend 怎么知道到哪个类的内部去找方法列表呢?— 就是通过 isa当实例方法被调用时,它要通过自己持有的 isa 来查找对应的类,然后通过其 class_data_bits_t 结构体查找对应方法的实现。 如果是调用父类方法,则可通过 super_class 指针用来查找继承的方法 – 看到这,我们可以利用我们的联想猜到我们开头的第 2 个问题 – OC 中面向对象编程的 继承 是怎么实现的。

实例对象可以通过 isa 找到对应的类,那么类方法的实现又是如何查找并且调用的呢?这时,就需要 “元类” 出马了。

让每一个类对象的 isa 指向对应的 元类,把类方法放到 元类 里面去!也就是说,实例对象的 isa 是类对象,而类对象的 isa 就是 元类!这样的设计太巧妙了!这样,无论是类还是对象都能通过相同的机制查找方法的实现了 (就是顺着其 isa 指针找):

  • 实例对象 发消息时,通过它自己的 isa 找到其 类对象,并从 类对象 的方法列表中获取方法的实现 (如找不到则通过 superclass 追溯其父类);
  • 而调用 类方法 时,通过 类对象 自己的 isa 找到其 元类,并从中获取方法的实现 (如找不到则通过 superclass 追溯其父类的元类!)

ios-runtime-oop-class_metaclass

最后再多说一句,正如我们开头所说的,OC 中,NSObject 是所有类的父类。所以上图的 RootClass 就是 NSObject,也就是 NSObject 的元类的父类,也还是 NSObject 类对象。最后定义 NSObject 类的父类为空,就可以用于结束继承方法的查找

3.3 小实验

最后我们用 cocoawithlove 文章中的小实验验证一下上面的内容,实验动态创建一个子类 RuntimeErrorSubclass,派生自 NSError。然后打印看看其类对象、元类等信息:

小实验
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
void ReportFunction(id self, SEL _cmd)
{
NSLog(@"This object is %p.", self);
NSLog(@"Class is %@, and super is %@.", [self class], [self superclass]);
Class currentClass = [self class];
for( int i = 1; i < 5; ++i )
{
NSLog(@"Following the isa pointer %d times gives %p", i, currentClass);
currentClass = object_getClass(currentClass);
}
NSLog(@"NSObject's class is %p", [NSObject class]);
NSLog(@"NSObject's meta class is %p",object_getClass([NSObject class]));
}

// 动态创建一个类 RuntimeErrorSubclass,继承自 NSError
Class newClass = objc_allocateClassPair([NSError class], "RuntimeErrorSubclass", 0);
// 动态地给 RuntimeErrorSubclass 添加方法,并调用之
class_addMethod(newClass, @selector(report), (IMP)ReportFunction, "v@:");
objc_registerClassPair(newClass);

id instanceOfNewClass = [[newClass alloc] initWithDomain:@"some Domain" code:0 userInfo:nil];
[instanceOfNewClass performSelector:@selector(report)];

// 结果
// > This object is 0x103021390. (实例对象地址)
// > Class is RuntimeErrorSubclass, and super is NSError.
// > Following the isa pointer 1 times gives 0x103021cf0 (打印的是 RuntimeErrorSubclass 类实例的地址)
// > Following the isa pointer 2 times gives 0x103021d20 (访问 RuntimeErrorSubclass 类实例的 isa,得到 RuntimeErrorSubclass 元类地址)
// > Following the isa pointer 3 times gives 0x7fff8ec730f0 (访问 RuntimeErrorSubclass 元类的 isa,因为所有元类的元类都是 NSObject 元类,所以此为 NSObject 元类的地址)
// > Following the isa pointer 4 times gives 0x7fff8ec730f0(因为 NSObject 元类的元类指向自己)
// > NSObject's class is 0x7fff8ec73140
// > NSObject's meta class is 0x7fff8ec730f0( 打印 NSObject 元类地址,证明了我们之前的解释)

这里小解释一下,对象的 [obj class] 方法得到的是实例对象的 类对象。而 object_getClass (class) 为访问其 isa 指针,得到的是 元类

4 小结

OC封装 这个特性是靠类来实现的,类在 Runtime 中是 objc_class 结构体。其核心成员 class_data_bits_t 是一个复合指针,指向类核心封装数据结构 class_rw_t。而 class_rw_t 采用了 2 层设计,外层存放了一些可扩展的成员列表。内层 class_ro_t 是类的只读基本信息。

继承 的实现在于属性与方法的复用。实例对象成员的获取好理解,因为实例对象就是 objc_class 实例直接从 class_rw_t 中的列表中去取就行了。但是因为方法的复用是放在类中的,OC 中运用了 元类 的概念,将实例对象的方法放在 元类 中。方法的调用使用 消息传递 机制,到 元类 中去获取。

多态 的实现细节可以作为一个思考题,大家基于上面消息传递机制的图其实很容易就可以想通了。

最后附上我画了 OC 中 OOP 的基本实现类图结构,方便大家更直观地理解:

OC 中 OOP 的基本实现类图结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
                                                              ┌──────────────────┐
┌────────────────┐ unitptr_r bits │ bits │
│ objc_object │ ┌──── ── ── ───── ── ── ───── ── ── ──▶ ├──────────────────┤
├────────────────┤ │ Class cls ┌── ─── ── ── ───┐ │ has_assoc │
│ isa_t isa ─────┼────┴──────────────▶│ metaClass │ │ weakly_referenced│
└────────▲───────┘ └── ── ── ─── ───┘ │ has_sidetable_rc │
│ │ ... │
│ └──────────────────┘
┌─────────────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ objc_class │ │ class_rw_t │ │ class_ro_t │
├─────────────────────────┤ ├──────────────────┤ ├──────────────────┤
│ Class superclass │ │ flags │ │ name │
│ cache_t cache │ │ version │ │ ... │
│ │ │ .... │ │ baseMethodList │
│ class_data_bits_t bits ─┼─────▶│ methods │ │ baseProtocols │
│ │ │ properties │ │ baseProperties │
└─────────────────────────┘ │ protocols │ │ │
│ │ │ ivars │
│ class_ro_t *ro ──┼───▶│ ivarLayout │
│ │ │ weakIvarLayout │
└──────────────────┘ └──────────────────┘

5 引用

  1. [《从 NSObject 的初始化了解 isa》- Draveness](https://github.com/Draveness/analyze/blob/master/contents/objc/ 从 %20NSObject%20 的初始化了解 %20isa.md),Draveness 关于 Runtime 的系列文章从源码入手,干货满满,是非常有城意的系列文章,强烈推荐大家阅读之。本文详细分析了 isa 指针。
  2. 《Objective-C runtime 机制 (1)—— 基本数据结构:objc_object & objc_class》- slunlun 分析 Runtime 源码讲的很好的一篇文章,条理很清楚。
  3. http://www.cocoawithlove.com/2010/01/what-is-meta-class-in-objective-c.html ,cocoawithlove 关于元类的一篇文章
  4. 《iOS-Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法》 我学习本系列的主要参考书之一

本章开始,我们来一起学习Pandas这个库,之前介绍过,pandas是数据处理过程中非常重要的一个基础库。它拥有SeriesDataFrame两大超实用的数据结构,使之可以方便地处理表格和其它复杂数据。pandas工作主要在数据清洗与分析上。但是,说得高大上,实质上,Pandas就是对各种表进行了抽象,以方便大家对表格进行处理的工具。

在介绍Pandas之前我们先来看看一张数据表,同时了解下pandas两个主要的数据结构DataFrame & Series,本章将先详细介绍Series,下一章再介绍DataFrame

一个图示看懂Series与DataFrame

阅读全文 »

在上一章《pandas基础数据结构-Series》我们提到过,pandas其实就是个功能强大的代码版Excel。其中的SeriesDataFrame就是用于抽象表及封装表的各种高级处理方法的数据结构。我们已经介绍完表示记录内容的Series,这章就主要说说DataFrame,看看他是怎么抽象表示表框架,以及可以对表格做怎样的处理。

学完本章,你就可以发现,DataFrame就是抽象了表结构,拥有表结构所需要的各种属性及操作一个表结构的各种方法。而Series,抽象了组成表内容的列向量与行向量。通过本章学习后,你会更加了解这段话。

阅读全文 »

1 前言

前段时间因为研究了一下量化的东西,接触到了python科学计算相关的内容。结果是猛然发现自己以前的统计学都白学了,很多相关的知识都还给了老师。

然后我就找来本统计学的书看,看了一段后,忽然发现,使用python结合书本里的东西一起看,边看边写代码,觉得自己的理解比以前深多了 – 为什么以前在学校里老师教概率论与统计学时不一边使用python工具来结合学习呢?我上网搜了一搜,发现系统讲这个的还真不多,而且自己也想记录下自己所学以加深自己的印象。于是就想边学边记,也一边强迫自己不要放弃。

结合python来学习统计学知识,本系列希望大家可以跟着我一起学习,主要是学习统计学知识,然后使用python来应用之,解决一些日常可能会用到的问题。我们会一边学习一边思考,希望可以将所学用在如投资交易、数据分析、数据化运营这种生活或工作中的应用上。

阅读全文 »

numpy提供了一系列的函数来方便对ndarray进行操作,这些函数基本上都有2个特点:

  1. 执行效率高;
  2. 元素级操作(即对数组中每个元素都进行运算操作)

这也就是说,当遇到此类需求时,使用ufunc可以提供高效而又方便的操作,日常进行数据处理时应该常用。

比如,对数组内的每个元素求平方根:

对数组内的每个元素求平方根
1
2
arr = np.arange(3)  # [0 1 2]
np.sqrt(arr) # [0 1 1.41421356]
阅读全文 »

上一章我们一起学习了numpy库的基础知识,以及np的主要数据结构ndarray

我们都知道,程序 = 数据结构 + 算法,这章中,我们一起学习下基于ndarray这个多维数组的一些基础运算。

本章所说的运算主要是numpy中ndarray的运算。在我们平常的编程工作中,数组的运算应该是用得最多的运算了,我们常常需要把数据放到数组里(也就是给数组赋值),遍历数组,对数组中数据进行处理等。在大数据处理中,数组的运算就更为重要了。下面介绍一些初级的数组运算,随着我们进一步的学习,后面会学习更多更高级运算以及结合统计学或线性代数的知识学习更多运用的场合。

学习时,可以边对照着下面的导图边学习各章内容,这样思路会更清晰。

阅读全文 »
0%