Blacktea's Life

A programmer who love music and football


  • 首页

  • 分类

  • 关于

  • 归档

  • 标签

  • 公益404

iOS性能解读——把时间当作朋友

发表于 2019-03-18 | 分类于 iOS | 阅读次数

保障良好性能三个层次

常用高效的工具和管理方法

刚开始我们的程序遇到性能问题时,我们会学习各种工具去分析程序性能,当性能出现明显问题时,这是最高效的解决方法,并且我们也能在这个过程中积累对代码性能的了解,这通常是点状的经验。后来我们发现性能问题还是层出不穷,于是我们开始对程序运行的核心链路做拆分,为拆分后的每个模块定义基线,数据跟踪,卡口流程。这和社会环保问题其实很像,性能出现问题时有高效的定位、解决问题的工具和能力,这相当于体育比赛后组织方有高效地清除垃圾方法和执行人员。性能基线和卡口流程相当于社会法律和执法体系,丢垃圾罚500、吐痰罚200.

了解本质是养成意识的前提

这通常都是最有效的管理方法,但却不是万能和高效的办法,通常只能在核心的业务做这种投入。建设管理设施和执行维护的成本非常高,就如识别谁丢了垃圾的成本就很高,而且很多时候你发现一片污染时,它可能是许多人一起作用的结果。要本质上去改善软件的性能,需要编写代码的人有良好的性能意识和习惯。就如新闻上常说的,日本的观众看完比赛,留下来的那片区域都非常收拾得很干净。想要真正形成意识、养成习惯,只喊口号和宣导是一种办法,但还不够。我们需要回归到本质,让大家真正认知到环保的好处,我们要把问题量化帮助大家理解, 让大家知道一套卖家的塑料饭盒具体处理的成本有多少。

对一砖一瓦要有明确认知

要形成具体地认知,我们得把性能问题拆解成基本元素,随着规模地上升,程序模块拆分越来越细、流程越来越交错,性能问题具体是哪个部件很容易被掩盖。幸好,程序再庞大也是一砖一瓦搭建出来的,归根结底是0和1两个数字的问题。当然0和1组合的可能性太多,超过了人脑的记忆能力。再上一层到了汇编,汇编对于稳定性问题来说是最确定性的协议,但对性能来说组合的复杂度还是过高了。
那再往上一层找,就到了高级语言最基础的运算和标准接口,这在iOS程序里是OC语言、C语言的基础操作。

保障良好性能三个层次

基础操作解析

下面首先基于OC和C语言的基本操作在iPhone6Plus设备的性能表现,弄清楚关键的性能代码的成本,再总结如何编写良好性能的代码。

测试用例:

复杂度为O(n^2),n在500到5000之间。

测试设备:

iPhone6Plus
1.85 GHz
Cores 2[7]
L1 cache Per core: 64 KB instruction + 64 KB data
L2 cache 3 MB shared[7]
L3 cache 4 MB shared[8]

算术运算

操作 Integer Arithmetic (n=5000) 平均操作时间(纳秒)
{} 3.316
{} 3.304
{} 3.291
k++ 0.185
k = i 0.397
k = i + j 0.598
k = i - j 0.655
k = i * j 0.685
k = i / j 0.736
k = i % j 0.524
k = i & j 0.763
k = i 或 j 0.716
k = i ^ j 0.407
k = ~i 0.139
k = i >> 2 0.189
k = i << 2 0.088

上图显示,每个空循环就需要3ns,这里的操作成本来自两层循环里变量自增、边界判断的消耗。因此,后面每项目操作测量的时间都减去了这个空方法的成本。

位运算毫无疑问是最快,只需要0.1ns左右。变量自增的成本比较低,算术运算差距都不大,其中除法运算和取模运算只比乘法要慢一点,这个有点不符合常识。在MacPro i7的模拟器上除法运算比乘法要慢5~10倍,应该是A9芯片对除法做了优化。

操作 Floating Point Arithmetic (n=5000) 平均操作时间(纳秒)
fj=j; 0.850
fj=j; fk = fi + fj; 1.037
fj=j; fk = fi - fj; 1.073
fj=j; fk = fi * fj; 1.049
fj=j; fk = fi / fj; 1.017

浮点变量的运算和整型相比差距不大。

操作 NSNumber Arithmetic (n=5000) 平均操作时间(纳秒)
numberJ = @(numberI.intValue + j) 13.717
numberJ = @(numberI.intValue - j) 13.405
numberJ = @(numberI.intValue * j) 13.377
numberJ = @(numberI.intValue / j) 13.465
numberJ = @(numberI.intValue % j) 14.792
numberJ = @(numberI.intValue & j) 13.401
numberJ = @(numberI.intValue 或 j) 13.436

NSNumber的算术运算比基础数据类型要慢了1个数量级,达到了10ns,装箱和拆箱的成本还是很高的,所以要尽量避免NSNumber的使用。

关系运算

操作 Comparisons (n=5000)| 平均操作时间(纳秒) |
|———————————|——|
| if (i < j) | 0.649 |
| if (i < j) k++ | 0.555 |
| if (x[i] < x[j]) k++ | 0.803 |

操作 Array Comparisons and Swaps (n=5000) 平均操作时间(纳秒)
k = (x[i]<x[k]) ? -1:1 6.663
k = intcmp(x+i, x+j) 1.532
swapmac(i, j) 1.692
swapfunc(i, j) 2.425

操作 Max Function, Macro and Inline (n=5000) | 平均操作时间(纳秒) |
|———————————|——|
| k = (i > j) ? i : j | 0.910 |
| k = maxmac(i, j) | 0.851 |
| k = maxfunc(i, j) | 1.029 |

比较和算术运算成本差不多,使用for(i..n)每次循环都要判断一次。同样的交换逻辑,使用宏展开比使用函数要快800us。因此,对于一些简单高频的计算,可以使用宏进行展开优化。

数学函数

操作 Math Functions (n=1000) 平均操作时间(纳秒)
fk = j+fi; 1.725
k = rand(); 7.700
fk = sqrt(j+fi) 1.965
fk = sin(j+fi) 13.524
fk = sinh(j+fi) 5.069
fk = asin(j+fi) 2.333
fk = cos(j+fi) 13.948
fk = tan(j+fi) 18.347

数学函数里开平方根还是很快的,而随机数和三角函数都上了一个数量级。

集合操作

操作 NSMutableArray Handle (n=1000) 平均操作时间(纳秒)
[array addObject:@”a”] 31.316
[array objectAtIndex:0] 4.708
[array containsObject:@”a”] 17.681
[array removeObject:@”a”] 65.235

NSMutableArray的操作明显比数组存取要耗时很多,如果是性能关键代码是普通C数组的,但是这样扩展性会比较差,需要自己管理引用计数,也不能享受NSMutableArray排序、搜索等便捷方法,所以一般情况还是建议使用NSMutableArray。

操作 String Handle (n=500) 平均操作时间(纳秒)
[string isEqualToString:@”b”] 114.632
[string stringByAppendingString:@”b”] 689.016
[string substringFromIndex:0] 165.271
[string compare:@”b”] 138.748
strcmp(str1, str2) - 2.029
strcpy(str1, str2) 13.995
strncpy(dest, str2, 1) 14.412

NString的的操作耗时比C字符串也要高1两个数量级,但是涉及可读文本是这些开销是值得的,否则你得自己处理Unicode,这本身就很复杂而且也很耗性能。当然如果只是用做功能字符串,完全可以用C字符串替代。

操作 NSMutableDictionary Handle (n=500) 平均操作时间(纳秒)
[dic setObject:@”a” forKey:@”b”] 98.359
[dic objectForKey:@”a”] 198.459
obj.a = @”b” 12.444
NSString *b = obj.a 7.948

NSMutableDictionary的键值访问比起对象的赋值、取值耗时要高1个数量级,1万次操作就达到1ms,并且如果有数据类型要存储,字典必须用NSNumber,NSNumber的操作本身就性能很差。
字典在项目中使用频率是较高的,在不需要动态性的高频场景应该使用对象替代NSMutableDictionary。一般网络请求json格式回来我们都会先通过NSJSONSerialization转成NSDictionary。业务数据量大的时候,成本显然不小。

OC Core Method (n=500) 操作 平均操作时间(纳秒)
[self emptyMethod] 4.951
[obj autorelease] 6.020

一个空的OC消息发送是算术运算的3倍,虽然实际项目中消息数量很容易爆炸,但只要不是千万级别以上的数量,也不会有性能的隐患。

内存分配和读取

操作 Memory Allocation (n=500) 平均操作时间(纳秒)
free(malloc(16)); 57.301
free(malloc(100)); 60.835
free(maloc(2000)); 130.475
[NSObject alloc] 49.398

堆内存的分配是消息发送的50倍,比算术运算要高2个数量级以上。一次性多分配更多内存比多次分配小内存要快很多,因为单次操作主存成本很高。(PS:在X86的模拟器里,对象创建成本是键值访问的两倍,但是在iPhone6Plus的表现,键值访问要反而慢两倍。)
这是一个重要的优化点,在性能关键代码里,高频对象可以提前做内存预分配和复用,以优化运行的速度。

  • A8 (iPhone6)
数据集大小 Stride 平均单次读取耗时 ns
64KB 1 0.114195
64KB 64 0.115699
64KB 5678 0.163618
1MB 1 0.116082
1MB 64 0.153635
1MB 5678 0.186276
4MB 1 0.114724
4MB 64 0.153919
4MB 5678 0.324870
200MB 1 0.216536
200MB 64 2.808878
200MB 5678 9.092147
  • A9 (iPhone6S Plus)
数据集大小 Stride 平均单次读取耗时 ns
64KB 1 0.030707
64KB 64 0.033846
64KB 5678 0.053130
3MB 1 0.030772
3MB 64 0.035724
3MB 5678 0.062342
4MB 1 0.031756
4MB 64 0.038277
4MB 5678 0.092065
200MB 1 0.034641
200MB 64 0.106000
200MB 5678 0.404821

L1、L2、L3缓存的数据集,顺序读取和跨缓存行读取性能基本相同,随机读取会慢50%~200%。由此看出,数据集比较集中并且在高速缓存大小范围内,访问的模式影响不太大,速度比较稳定。
当数据集超过L3时,访问到主存的概率会越来越大,这个时候访问的模式就有较大的差异。顺序访问时大部分的操作还是可以命中缓存行,耗时和前面差不多。步伐大于缓存行后,性能开始明显变差。在A8处理器下,最差到了顺序访问的50倍以上。A9处理器优化很明显,最差也就是顺序访问的10倍.

复杂度的陷阱

性能瓶颈离我们并不远

时间复杂度的几种规模,相信大家都很熟悉了。但我们要明确,什么样的复杂度在OC的世界里是有风险的。

下面这个表格可以直观看出各种复杂度的差距

运行时间(微秒) n^3 n^2 nLog2^n n Log2^n
10 1ms 100us 30us 10us 3.3us
当n为右侧规模时所需要的运算时间 10^2 1s 10ms 660us 100us 6.6us
10^3 16分钟 1s 10ms 1ms 9.9us
10^4 69天 1分钟 40秒 132ms 10ms 13.2us

假设我们编写的代码块里有NSDictionary的键值操作或者字符串拼接操作,操作耗时很容易到了微秒级别。

此时如果你方法的复杂度到了O(n^2),当n到了10^2时,整个运算就超过10ms,n到了10^3时,运算时间会到1s。

1
2
3
4
5
6
7
8
int n = 1000;
NSMutableDictionary *dic = [NSMutableDictionary new];
for (int i = 0; i < n; i ++) {
for (int j = 0; j < n; j ++) {
[dic setObject:@(j) forKey:@(i)];
}
}

例如上面这个case,只有一行的操作代码,就能达到100ms。而这个规模,在客户端的计算中是很容易出现的。所以我们要对嵌套的线性运算保持敏感,明白它的存在会有多大的风险。

被遗漏的二阶运算

即便是有控制复杂度地意识,我们很是会不小心在线性运算中嵌套线性运算,导致accidentally quadratic。要避免这种情况发生,我们需要做到一下两点,

第一:对常用系统方法的复杂度建立清晰的认知。

例如下面的Case。将对象添加到数组前使用indexOfObject进行去重判断,而indexOfObject是O(n)的,这直接导致整个操作复杂度升级到O(n^2)。

1
2
3
4
5
6
7
- (void)addObjects:(NSArray *)objects {
for (id object in objects) {
if (NSNotFound != [array indexOfObject:object]) {
[array addObject:object];
}
}
}

NSArray是有序的,它的查找、删除方法都需要遍历整个集合(containsObject,indexOfObject,removeObject),时间复杂度都是O(n)以上。

优化的办法是使用set,set使用了哈希值存储对象,containsObject复杂度是O(1),可以将整个操作恢复到线性。我们需要对常用集合每个方法的复杂度有清晰的认识,才能保证不会不小心写出O(n^2)以上的代码。

1
2
3
4
5
6
7
8
- (void)addObjects:(NSArray *)objects {
NSSet *set = [NSSet setWithArray:array];
for (id object in objects) {
if (![set containsObject:object]) {
[array addObject:object];
}
}
}

第二:注意方法嵌套代码,这很容易使我们忽略了整体的复杂度

在工程项目中,你的代码可能是像下面这样编写的,这就很难直观的看出有线性的嵌套。

1
2
3
4
5
6
7
8
9
10
11
- (void)addObjects:(NSArray *)objects {
for (id object in objects) {
[self addAbsentObject:object];
}
}
- (void)addAbsentObject:(id)object {
if (NSNotFound != [array indexOfObject:object]) {
[array addObject:object];
}
}

要避免这种失误,我们得加强代码的敏感度,看到关键方法要有直觉的反应。同时编写底层方法时,对有复杂度风险的方法加上明确的注释,不要让调用方跳入泥坑。

总结

1 记住关键操作的性能公式
原始操作:消息传递:对象创建 : 键值访问 = 1 : 10 : 100 : 200

2 权衡利用C语言的速度和OC的面向对象设计,c语言的集合类型比起OC确实要快上一两个数量级。

3 尽量用对象替代动态的NSDictionary,用基础数据类型替代需要装箱的NSNumber。

4 如果有高频次的对象分配,达到了10万甚至100万的数量,可以使用预分配内存加复用的方式来加速。

5 保持敏感,除非必要不要写出O(n^2)的代码块

6 遇到必要的复杂计算或IO,合理使用多线程发挥CPU多核的计算能力,减少主线程的卡顿

使用node在阿里云上创建一个简单的web服务

发表于 2018-06-03 | 分类于 Node | 阅读次数

创建Node项目

Demo
https://github.com/blackteachinese/blackteahome

阿里云ESC环境安装

部署Node服务

从React到UIView渲染过程解析

发表于 2017-12-24 | 分类于 web | 阅读次数

React

首先用React写一个HelloWorld组件,代码如下:

1
2
3
4
5
6
7
8
9
10
var HelloWorld = React.createClass({
getInitialState: function() {
return {type: 'say:'};
},
render: function() {
return React.createElement("div", null,this.state.type, "Hello ", this.props.name);
}
});
React.render(React.createElement(HelloWorld, {name: "John"}), document.getElementById("container"));

如果最终渲染到浏览器,HelloWorld组件会通过React.render进行解析,最后会变成一下的Render Tree

1
2
3
4
5
<div data-reactid="0">
<span data-reactid="0.0">say:</span>
<span data-reactid="0.1">Hello </span>
<span data-reactid="0.2">John</span>
</div>

但是如果是RN页面,是不需要解析为原生DOM元素的,因为RN页面的组件和Native的视图一一映射,最终是要解析为能让Native渲染并布局指定View的命令,具体请往下看。

Native 部分

1.React页面写完后,经过打包成为JSBundle文件,到这里前端的工作告一段落了。
RN页面的初始化是从Native发起的,Native使用JS引擎(JavaScriptCore/V8)将JSBundle加载到客户端的JS运行环境JSContext里。接着Native会将提供给JS的各个Module的Api转化成配置列表,注入到JSBundle里。这份配置里其中有一个Module较UIManage,UIManage里包含了Native所有View的映射列表,并提供了创建View、设置子View等视图布局的API。

所有Module的列表

UIManage配置信息

2.配置文件注入到JSBundle之后,Native会告诉JS可以开始运行页面,JS收到通知后开始运行HelloWorld组件的Render方法,根据UIManage的配置文件将每个组件解析为UIManage的API调用,然后将API调用列表以批处理的方式,通过JSBridge调用Native来执行。
下面是其中一个组件的Api调用Json:

buffer

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
<__NSArrayM 0x60c00024b8e0>(
51
)
,
<__NSArrayM 0x60c00024b730>(
5
)
,
<__NSArrayM 0x60c00024b850>(
<__NSArrayM 0x60c00024b550>(
3,
RCTText,
1,
{
accessible = 1;
allowFontScaling = 1;
color = 4286846340;
ellipsizeMode = tail;
fontSize = 18;
padding = 8;
textAlign = center;
}
)
)
,
1
)

moduleIDs
根据上面的Module列表,’51’就是UIManage

1
2
3
<__NSArrayM 0x60c00024b8e0>(
51
)

methodIDs
根据上面的UIManage配置信息,‘5’就是CreateView方法

1
2
3
<__NSArrayM 0x60c00024b730>(
5
)

paramsArrays
RCTText是Native对应的View,其他就是布局参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<__NSArrayM 0x60c00024b850>(
<__NSArrayM 0x60c00024b550>(
3,
RCTText,
1,
{
accessible = 1;
allowFontScaling = 1;
color = 4286846340;
ellipsizeMode = tail;
fontSize = 18;
padding = 8;
textAlign = center;
}
)
)

最后执行完所有API,所有的子视图都会被添加到了RCTRootView里,css和flexbox的布局信息,经过Native计算过后,会变成最后的绝对布局。
到这里为止一个React页面完整的渲染过程就结束了。

如何设计一个iOS统一持久化SDK

发表于 2017-12-11 | 分类于 iOS | 阅读次数

设计一个统一持久化SDK的意义

提高可维护性和安全性。
没有统一的存储层,业务代码里存储功能会有很多重复代码,不仅维护性差、安全性也无法保障

提高存储质量和性能。
不同业务场景适合使用的存储方式是不一样的,为了提高性能我们得使用复合式的存储策略,为了保证稳定性还得考虑监控

不同场景的存储方式

KV存储

kV存储是最常用的存储方式,超过80%的业务存储会用到,它是一种NoSQL(非关系型数据库)模型搜索,适用于非复杂业务关系的场景。KV存储又包含5种类型分别是内存KV、文件KV、数据库KV、二级索引KV、流式大文件KV。

内存KV

内存KV是在内存上存储,也是最快速的存储方式,通常文件KV、数据库KV也会实现内存KV进行对象级别的加速。
内存KV的实现要考虑几个关键点,接口设计、清理机制、淘汰机制、并发管理。

淘汰机制
具体实现上建议使用LRU淘汰机制,它的核心思想是“最近用到的数据被重用的概率是最高的”,因此我们需要对缓存进行排序,并且保证位置调整的高性能,很自然地我们会想到使用双向链表来实现。查找一个链表中元素的时间复杂度是O(n),每次命中的时候,我们就需要花费O(n)的时间来进行查找,如果不添加其他的数据结构,这个就是我们能实现的最高效率了。我们还可以组合上hash表,它查找时间复杂度是O(1)。
最后我们可以设计一个数据结构,里面包含一个hashmap和双向链表,使用hashmap查询cache,使用双向链表做排列和位置管理。

清理机制
手机设备内存是奢侈品,在内存紧张时,我们要有清理缓存的机制

并发管理
为了保证多线程调用的安全性,增删改查每项操作我们都要加锁

数据库KV

数据大小在20K~30k以内,使用数据库存储效率较高,数据库KV的安全保障,要支持数据库级别的加密。

文件KV

数据大小大雨20K~30k,适合使用文件KV,需要提供文件校验、加密能力。

二级索引KV

在文件KV的基础上,使用两个key来指定一个value,适合图片库这种需要基于图片url和尺寸信息的场景。

流式文件KV

针对视频等较大的流式文件,如果我们对整块数据进行存取会消耗大量内存,需要进行分段处理。

ORM存储

ORM是对象关系映射Object Relational Mapping,它的核心是将复杂的数据结构对象化,将sql操作转化为简单易用的的面向对象操作。
ORM的开发需要考虑到各种feature,多线程安全、事务、时间模型、列级加密、支持批处理操作、对象域,支持对对象管理方式的控制等。

DB储存

需要进行复杂数据的操作的存储场景,一般就要直接用到DB,DB的设计是一门大学问,要考虑的要点有很多。比如线程池管理、读写互斥、并发控制、任务调度,数据库的加密,Api的易用(SQLite3难用)。其中SQLite3免费版不支持加密的,但它是有预留加密数据库的接口,可以自己实现加密功能。如果要追求极致的性能,在SQL的查询还可以做缓存功能。

执行计划缓存

SQL响应时间包括两部分:RT = 解析时间和 + 执行时间。
如果将执行计划进行缓存可以减少解析的时间,如果将结果集进行缓存,可以减少执行时间。But,由于查询的结果集可能很多,而且数据精彩会被修改,结果集缓存意义不大,因此我们主要考虑对执行计划进行缓存。

相识的sql可以使用同一个执行计划,比如“select from t where num=1”和“select from t where num=2”。因此我们可以先将语句标准化(忽略大小写,忽略空格数目,将常量全部抽象成 ?),减少缓存数量,最后还要考虑淘汰机制,整体思路如下。
1对SQL进行词法语法分析,抽象出常量
2查询执行计划缓存是否命中,若不命中,则进行解析
3将常量绑定到执行计划

监控

TODO

ReactNative到底做了什么?

发表于 2017-11-26 | 分类于 iOS | 阅读次数

React Native 是什么

react.js是一个流行的前端开发框架,相信大家都有了解,它用了flexbox布局技术、jsx等语法糖的技术,这让前端同学能象喝茶一样轻松地开发H5页面。React本来是用来开发h5页面,facebook的工程师们,希望做一套能同时运行在iOS和Android的高性能开发框架,就将React render()方法渲染出来的js代码通过JSbridge加载到native,native再通过JSCore和V8这两个js解析引擎,解析为native的页面。

React Native 的组成

ReactNative功能结构可以分为两部分,JS模块和Native模块。JS模块和Native模块都有Bridge组件,两端的Bridge遵守一套通信协议、通信机制,从而实现了js和native的无缝连接。

JS模块

JS模块主要由React.js框架和JSBridge两部分组成,React.js是开发UI逻辑的框架,而JSBridge包含管理NativeModel配置的NativeModules和管理js、native双向通信的MessageQueue。

JSBridge如何匹配和调用Native提供的能力

Nativebridge初始化时会把Native能提供给JS的所有能力Module按约定好的方式序列化为一个对象‘__fbBatchedBridgeConfig’,设置到JS的全局对象global里。

JSBridge封装了NativeModules对象,NativeModules会读取__fbBatchedBridgeConfig的bridgep配置,组装成方便js调用的映射对象.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let NativeModules : {[moduleName: string]: Object} = {};
const bridgeConfig = global.__fbBatchedBridgeConfig;
(bridgeConfig.remoteModuleConfig || []).forEach((config: ModuleConfig, moduleID: number) => {
// configuration of the module will be lazily loaded.
const info = genModule(config, moduleID);
if (info.module) {
NativeModules[info.name] = info.module;
}
// If there's no module config, define a lazy getter
else {
defineLazyObjectProperty(NativeModules, info.name, {
get: () => loadModule(info.name, moduleID)
});
}
});

js组件通过methodName可以调用Native提供的各项能力。

1
NativeModules.DialogManagerAndroid.showAlert();

MessageQueue实现了Native call JS的Api和js call native的机制

MessageQueue里有4个暴露给Native调用的API

1
2
3
4
flushedQueue()
invokeCallbackAndReturnFlushedQueue(cbID: number, args: any[])
callFunctionReturnFlushedQueue(module: string, method: string, args: any[])
callFunctionReturnResultAndFlushedQueue(module: string,method: string,args: any[],)

Native想要调用JS的方法
JS也会通过Module封装提供给Native调用的组件和方法,当native需要主动调用时就会call”callFunctionReturnFlushedQueue”和“callFunctionReturnResultAndFlushedQueue”,并传递module、method做为找到对应方法的钥匙。

JS想要调用Native的方法
事实上JS不能直接调用Native的方法,RN里设置了被动调用的机制来实现相同的功能。
当JS要调用Native的方法时,会通过enqueueNativeCall把moduleID和methodID放到队列this._queue里,调用成功和失败的Callback放到this._successCallbacks和this._failureCallbacks里。

1
enqueueNativeCall(moduleID: number,methodID: number,params: any[],onFail: ?Function,onSucc: ?Function,)

每次Native调用JS时,都会把this._queue的消息通过返回值传递给Native,Native拿到消息队列后就会执行MessageQueue里对应的Native方法。

TODO: js要调用Native时方法注入queue的时机?

调用Native方法执行后的回调
native暴露给js调用的方法,可以使用RCTResponseSenderBlock对象做为参数实现回调的功能。RCTResponseSenderBlock执行最后会调用到invokeCallbackAndReturnFlushedQueue方法,回调MessageQueue里成功或失败的回调。

JS消息队列超过5ms还未被调用
当JS通过enqueueNativeCall注入需要被Native调用的方法时,需要等待native来拉去消息队列。如果native一直不过来拉消息怎么办呢?在enqueueNativeCall注入时有一个保障机制,如果距离上次消息队列被拉去超过5ms,就会调用‘global.nativeFlushQueueImmediate(queue)’方法,启用JS强制让Native拉去的机制。

1
2
3
4
5
6
7
if (global.nativeFlushQueueImmediate && (now - this._lastFlush >= MIN_TIME_BETWEEN_FLUSHES_MS || this._inCall === 0)
) {
var queue = this._queue;
this._queue = [[], [], [], this._callID];
this._lastFlush = now;
global.nativeFlushQueueImmediate(queue);
}

Native部分(OC)

native部分可分为OC bridge和一系列基础Module和自定义的扩展Module.

Module
每一个Module都会根据约定的协议去注册,Bridge初始化时就会读取Navtive所有的Module,序列化为配置表,并将这部分配置信息传递到JS模块,JS也会生成一一对应配置表,这样无论是js想调用native的api,还是native想调用js的api都可以找到。

NativeBridge
Nativebridge需要做很多事情,它包含了JSbundle加载、初始化缓存Module组件、管控js和oc之间消息传递的分发。

下面是NativeBridge初始化的过程

  • RCTRootView 根视图
    • RCTBridge 桥的抽象类。持有BatchBridge,将核心逻辑转发给BatchBridge实现
      • RCTBatchBridge 负责主要核心功能的初始化
        • RCTJavaScriptLoader 加载JSBundle
        • RCTDisplayLink 提供屏幕渲染频率的回调,为timer和桢动画等组件提供支持
        • RCTModuleData 所有的RN组件UI或Api,都是RCTModuleData
          • RCTJSCExecutor 一个特殊的RCTModuleData,维护一个独立线程处理JS代码执行和js回调,是bridge的核心通道
          • RCTEventDispatcher UIModule触发点击事件后先通过EventDispatcher的api进行事件分发,再传到JSCExecutor找到JS模块的回调函数,最后通知js进行处理

Module配置表生成流程解析

RCTModuleData是组装native的功能模块,提供给JS调用

native导出注册的Modules给JS

RCTRootView初始化流程

  • 初始化Module
  • 生成Module配置表
  • 将Module配置表注入到JS里
  • JS读取Module配置

初始化Module
从RCTModuleClasses拿到所有Moduled的Class,各自创建RCTModuleData,并将创建后的module保存到moduleClassesByID、moduleDataByID、moduleDataByName三个集合进行缓存

生成Module配置表
RCTBatchBridge会循环moduleDataByID数组表,把每一个APIModule的name都写进数组,然后写进key为remoteModuleConfig的字典,最后序列化成JS

1
2
3
4
{"remoteModuleConfig":[["VKAlertModule"],
["RCTFileRequestHandler"],
["RCTDataRequestHandler"],
...]}

moduleConfigInject

将所有module名注入到__fbBatchedBridgeConfig

1
{"remoteModuleConfig":[["JSCExecutor"],["AccessibilityManager"],["ViewManager"],["ActivityIndicatorViewManager"],["AlertManager"],["AppState"],["AsyncLocalStorage"],["BlobModule"],["Clipboard"],["DataRequestHandler"],["DatePickerManager"],["DeviceInfo"],["DevLoadingView"],["DevMenu"],["DevSettings"],["EventDispatcher"],["ExceptionsManager"],["FileRequestHandler"],["HTTPRequestHandler"],["I18nManager"],["JSCSamplingProfiler"],["KeyboardObserver"],["MaskedViewManager"],["ModalHostViewManager"],["ModalManager"],["NavigatorManager"],["NavItemManager"],["NetInfo"],["Networking"],["PerfMonitor"],["PickerManager"],["PlatformConstants"],["ProgressViewManager"],["RawTextManager"],["RedBox"],["RefreshControlManager"],["SafeAreaViewManager"],["ScrollContentViewManager"],["ScrollViewManager"],["SegmentedControlManager"],["SliderManager"],["SourceCode"],["StatusBarManager"],["SwitchManager"],["TabBarItemManager"],["TabBarManager"],["TextFieldManager"],["TextManager"],["TextViewManager"],["Timing"],["TVNavigationEventEmitter"],["UIManager"],["WebSocketExecutor"],["WebSocketModule"],["WebViewManager"]]}

遍历所有Module,将每个Module被注册的method配置,注入到JSContext的nativeRequireModuleConfig里。的block注入到js的global全局对象里

1
2
3
4
5
context[@"nativeRequireModuleConfig"] = ^NSArray *(NSString *moduleName) {
RCTJSCExecutor *strongSelf = weakSelf;
NSArray *result = [strongSelf->_bridge configForModuleName:moduleName];
return RCTNullIfNil(result);
};

JS读取Module配置

JS加载时NativeModules.js会执行一段脚本,先通过global.__fbBatchedBridgeConfig获取Native所有Module的列表,再通过’global.nativeRequireModuleConfig’方法拿到Native所有Module的配置。

1
2
3
4
5
6
7
8
9
10
const bridgeConfig = global.__fbBatchedBridgeConfig;
invariant(bridgeConfig, '__fbBatchedBridgeConfig is not set, cannot invoke native modules');
const defineLazyObjectProperty = require('defineLazyObjectProperty');
(bridgeConfig.remoteModuleConfig || []).forEach((config: ModuleConfig, moduleID: number) => {
const info = genModule(config, moduleID);
defineLazyObjectProperty(NativeModules, info.name, {
get: () => loadModule(info.name, moduleID)
});
});

注册Module

RCT_EXPORT_MODULE()

导出方法

RCT_EXPORT_METHOD()

ReactNativeUI渲染解析

React开发的页面,最终都会解析回原生的JS标签,最终组装成JSBundle,Native初始化后会加载JSBundle,并将UI逻辑一一解析成Native的原生控件,整个页面会拼装为一个Native的RootView。
这个过程需要解析JS的UI逻辑,于是RN定一个一个Module专门用来做UI解析,它就是UIManage,在JS是UIManager.js,在Native时RCTUIManager和RCTComponentData。
RCTComponentData组装Native各种视图控件,将所有视图组件的配置信息提供给JS,js根据native提供的配置信息,创建Native视图。

UIModule的组成
UIModule包含RCTView和RCTViewManage,每个UIModule还对应一个RCTComponentData。RCTViewManage相当于Controller了,决定如何绘制RCTView。

管理UI组件生命周期的RCTUIManager和RCTComponentData
RCTUIManager是一个ApiModule,所以他是被RCTModuleData管理,并被RCTBatchBridge持有。

RCTUIManager在初始化的时候setBridge方法会被调用,在setBridge里会遍历所有的Module,找到所有继承自RCTViewModule的对象,然后以ModuleName为key缓存到_componentDataByName里

UI的创建和修改
React.js里UI组件由各个component组成,component使用flexbox进行布局,最后会转换成绝对的位置、样式。
Native里会对React的每个组件都实现对应的一个View,每一个View会继承RCTViewManager,并通过RCT_EXPORT_MODULE注册成为一个module,我们可以称他们为UIModule。
当一个JS Component对象需要创建/改变自己的样式时,React会把需要渲染的所有component生成对应的JS Component配置,Native根据配置找到对应的RCTView,根据配置信息初始化Native的UI组件。

RN一个页面的渲染流程

1.[RCTRootView runApplication:bridge]
通知JS准备好可以开始渲染

1
2
3
4
5
6
7
8
9
10
11
12
- (void)runApplication:(RCTBridge *)bridge
{
NSString *moduleName = _moduleName ?: @"";
NSDictionary *appParameters = @{
@"rootTag": _contentView.reactTag,
@"initialProps": _appProperties ?: @{},
};
[bridge enqueueJSCall:@"AppRegistry"
method:@"runApplication"
args:@[moduleName, appParameters]
completion:NULL];
}

2.由JS调用createView方法创建每一个Native的View,这个方法里会根据viewName找到对应的RCTComponentData。
调用RCTComponentData的createShadowViewWithTag创建shadow view,缓存到_shadowViewRegistry里。
调用RCTComponentData的createViewWithTag创建view,缓存到_viewRegistry里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
RCT_EXPORT_METHOD(createView:(nonnull NSNumber *)reactTag
viewName:(NSString *)viewName
rootTag:(nonnull NSNumber *)rootTag
props:(NSDictionary *)props){
RCTComponentData *componentData = _componentDataByName[viewName];
// Register shadow view
RCTShadowView *shadowView = [componentData createShadowViewWithTag:reactTag];
if (shadowView) {
[componentData setProps:props forShadowView:shadowView];
_shadowViewRegistry[reactTag] = shadowView;
RCTShadowView *rootView = _shadowViewRegistry[rootTag];
shadowView.rootView = (RCTRootShadowView *)rootView;
}
__weak RCTUIManager *weakManager = self;
RCTExecuteOnMainQueue(^{
RCTUIManager *uiManager = weakManager;
UIView *view = [componentData createViewWithTag:reactTag];
if (view) {
[componentData setProps:props forView:view]; // Must be done before bgColor to prevent wrong default
uiManager->_viewRegistry[reactTag] = view;
}
}

3.调用_layoutAndMount对RCTRootView及其子视图进行布局。
在RCTUIManage->uiBlockWithLayoutUpdateForRootView->collectViewsWithUpdatedFrames,将flexbox布局协议解析为绝对布局的Frame,将每个视图的frame设置到对应的shaowView里。

4.根据组件视图关系,由JS调用“setChildren”给每个View设置子Views。
其中创建一个View都会对应创建一个RCTShadowView,RCTShadowView保存View的布局关系和属性,管理View的加载

1
2
RCT_EXPORT_METHOD(setChildren:(nonnull NSNumber *)containerTag
reactTags:(NSArray<NSNumber *> *)reactTags)

5.通过RCTShadowView的processUpdatedProperties,调用每个View的didUpdateReactSubviews方法,添加各自的子视图。

1
2
3
4
5
6
- (void)didUpdateReactSubviews
{
for (UIView *subview in self.reactSubviews) {
[self addSubview:subview];
}
}

布局计算核心算法
我们写React时使用的是flex布局或绝对布局,这部分布局代码会随着jsbundle通过Native的JS引擎加载。但Native是识别不了Flex布局协议,所以需要通过facebook的yoga开源库来解析flex布局协议成为绝对Native可以识别的绝对布局。

  • YGNodeCalculateLayout
    • YGLayoutNodeInternal
    • YGNodelayoutImpl
      • YGNodeWithMeasureFuncSetMeasuredDimensions 计算size
      • YGNodeComputeFlexBasisForChild 计算child的布局

UI组件事件响应
当RCTView组件触发了点击、滑动等触摸事件时,会通过bridge找倒自己的JSComponent,根据预先缓存的js callback函数,将参数传递给对应的React组件进行响应。

自定义UIModule组件
1 创建RCTViewManager子类
a注册Module,使用RCT_EXPORT_MODULE注册宏
b实现视图创建的方法,’-(UIView *)view’
cJS层导入Native原生组件。在js模块的requireNativeComponent.js里操作UIManage导入。
2 创建属性
注册属性,使用RCT_EXPORT_VIEW_PROPERTY或RCT_CUSTOM_VIEW_PROPERTY
3 创建事件
注册事件,也是通过RCT_EXPORT_VIEW_PROPERTY,注册type为RCTBubblingEventBlock的事件属性
4 创建常量
a.native通过constantsToExport方法return一个NSDictionary注册常量
b.js通过UIManager.XXUIModule.Constants拿到注册的常量

Ref

react-native
React Native通信机制详解
yoga
ReactNative源码分析从源码一步一步解析它的实现原理
W3C 标准的 Flexbox 模型
CSS Flex Layout 算法解析

React源码的秘密

发表于 2017-11-24 | 分类于 iOS | 阅读次数

What is React

A JavaScript library for building user interfaces

基本概念

  • VIRTUAL DOM
    React render执行的结果并不是真正的DOM节点,只是是轻量级的JavaScript对象,我们称之为virtual DOM。虚拟DOM是React的一大亮点,具有batching(批处理)和高效的Diff算法。使用Virtual dom在性能上不会比纯手动优化的DOM操作更快,使用Virtual DOM能在不需要手动优化的情况下,保证过得去的性能,从而提高代码的可维护性和开发效率。这让我们可以无需担心性能问题而随时“刷新”整个页面,由虚拟DOM来确保只对界面上真正变化的节点进行实际的DOM操作。

下图是浏览器的工作流:

a.Create DOM tree:浏览器的渲染引擎根据html的elements,生成Dom node,组成Dom tree
b.Create Render tree:浏览器解析外部CSS文件和元素的inline样式,结合Dom tree的nodes,生成render tree
c.layout:render tree上每一个node生成在屏幕上的具体位置的坐标值
d.painting:绘制

如果直接操作Dom,每次操作都会触发整个渲染流程,如果连续修改50个节点,那就会造成50次重新渲染,这会造成不必要的渲染消耗。我们可以自己编写逻辑,将每个Dom操作汇总到一个Dom fragment,再传递给Dom tree。但这样我们就得自己去记录哪些节点改变,哪些没有改变,这显然开发效率太低,所以我们可以把这个工作抽象一下,于是就有了virtual dom。
virtual dom这个抽象层的作用就是将这件事自动化、抽象化,通过Diff算法计算出需要改变的节点,使用batching批处理多个节点的改动,然后操作Dom树进行渲染,避免不必要的重新渲染。

  • JSX
    JSX just provides syntactic sugar for “React.createElement(component, props, …children)”
    JSX是一个语法糖实际上就是封装了React.createElement,使用JSX可以提高代码的可读性和开发效率
  • Component
    无论是复杂的元素还是简单的元素,都定义为组件。通过组合组件的方式可以构建容易维护、高复用性的组件。
  • one-way reactive data flow
    React 的单向数据流的设计让前端 bug 定位变得简单,页面的UI和数据的对应是唯一的,我们可以通过定位数据变化就可以定位页面展现问题。

React Render Mechanism 渲染机制源码解析

Render analyse

React有3种element,Text element、Basic element 基本元素、Custom element 自定义元素。

Render DOM implement analyse

Basic element 基本元素渲染解析

1
2
3
4
Basic element example:
var element = React.createElement('div',{id:'test',onclick:hello},'click me')
React.render(element,document.getElementById("container"))
  1. 创建虚拟DOM实例
    React.createElement:

use createElement function to create a virtual dom -> React.createElement

  • keep the key to identify element
  • copy the config(attributes) to the props
  • copy the children to the props.children
  • call ReactElement function and send ‘type’,’key’,’props’ for initial a element object

2.将创建好的虚拟DOM实例通过React.render进行渲染
React.render:

  • instantiateReactComponent
    • init component instance
  • mountComponent
    • render component instance to html content

3.在React.render里,首先调用instantiateReactComponent方法,根据element渲染出component实例。

instantiateReactComponent:

  • return ReactDOMTextComponent
    • if(typeof node === ‘string’ || typeof node === ‘number’)
  • return ReactDOMComponent
    • if(typeof node === ‘object’ && typeof node.type === ‘string’)
  • return new ReactCompositeComponent
    • if(typeof node === ‘object’ && typeof node.type === ‘function’)

4.接着调用mountComponent,将instantiateReactComponent得到的component实例渲染成原生的Dom结构

ReactDOMComponent.prototype.mountComponent:

  • assign type : tagOpen = ‘<’ + ‘dic’
  • add props : tagOpen += propKey + props[propKey]
  • recursive child node to be content : each(content += childComponentInstance.mountComponent)
  • result : tagOpen + ‘>’ + content + tagClose

React的伪代码

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
// ReactElement:创建虚拟DOM
function ReactElement(type,key,props){
this.type = type;
this.key = key;
this.props = props;
}
// mountComponent:渲染component,生成的dom结构
ReactDOMTextComponent.prototype.mountComponent = function(rootID) {
this._rootNodeID = rootID;
return '<span data-reactid="' + rootID + '">' + this._currentElement + '</span>';
}
// instantiateReactComponent:生成component
function instantiateReactComponent(node){
......
}
React = {
createElement:function(type,config,children){
...
return new ReactElement(type, key,props);
},
createClass:function(spec){
...
},
// 渲染入口
render:function(element,container){
var componentInstance = instantiateReactComponent(element);
var markup = componentInstance.mountComponent(React.nextReactRootIndex++);
}
}

渲染结果
HelloWorld组件通过React.render进行解析最后变成Render Tree

1
2
3
4
5
<div data-reactid="0">
<span data-reactid="0.0">say:</span>
<span data-reactid="0.1">Hello </span>
<span data-reactid="0.2">John</span>
</div>

渲染流程图

Render Composite implement analyse

Custom element自定义元素渲染解析

自定义元素的渲染流程有两点不一样,第一增加了创建自定义类的环节,第二,在最后一步“mountComponent”会递归调用,将父组件的每一个子组件进行渲染解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var HelloMessage = React.createClass({
getInitialState: function() {
return {type: 'say:'};
},
componentWillMount: function() {
console.log('我就要开始渲染了。。。')
},
componentDidMount: function() {
console.log('我已经渲染好了。。。')
},
render: function() {
return React.createElement("div", null,this.state.type, "Hello ", this.props.name);
}
});
React.render(React.createElement(HelloMessage, {name: "John"}), document.getElementById("container"));

1.创建自定义类
React.createClass

  • Constructor a child class
  • inserit to ReactClasss
    • Constructor.prototype = new ReactClass();
  • extend Constructor.prototype,spec

ReactCompositeComponent.prototype.mountComponent

  • initialize public class by ‘ReactClass’ function
    • var inst = new ReactClass(publicProps);
  • life cycle callback
    • inst.componentWillMount()
  • call render to instance a element
    • renderedElement = inst.render() 返回的可能是一个DOM element
  • getting component instance
    • renderedComponentInstance = instantiateReactComponent(renderedElement)
  • get rendered result by renderedComponentInstance
    • renderedComponentInstance.mountComponent
  • life cycle callback
    • inst.componentDidMount()

React Native ReRender mechanism 重新渲染机制

一个react组件的重新渲染,是通过setState方法的调用,导致stated改变发起的
ReactClass.prototype.setState

  • call ‘this._reactInternalInstance.receiveComponent(null, newState);’

All the component implement the ‘receiveComponent’ the handle the render of themself. the DOMComponent is the most complexest ,it used diff algorithm to handle the child node update.

state改变后会调用组件的receiveComponent方法进行element和属性的更新

ReactCompositeComponent.prototype.receiveComponent

  • update element 更新element对象
  • bind the new state and props 生成心的state和props
  • judge whether update or reRender the element by type and key

ReactDOMTextComponent.prototype.receiveComponent

  • if the text string is change, update content of the node 如果text的内容有变化,刷新节点

ReactDOMComponent.prototype.receiveComponent

  • update element 更新element对象
  • updateDOMProperties 更新属性
  • updateDOMChildren 更新子节点

implementation of DomComponent RecevieComponent

首先是更新Properties

ReactDOMComponent.prototype._updateDOMProperties

  • remove the old attribute
  • remove the event monitor
  • add the new attribute
  • add the new event monitor

接着是更新子组件

ReactDOMComponent.prototype._updateDOMChildren

  • diff
    • user diff algorithm the find out diffrence and add the diffrence to the diffQueue
  • patch
    • traversal diffQueue for removing the changed node , inserting the new note and inserting the modified node

analyse diff and patch

更新子组件时,使用diff算法计算出需要移动、插入、删除的组件,大部分没有变动的组件不做更新。

ReactDOMComponent.prototype._diff

  • getting the previous component of childrend
  • generating the next component of childrend
  • assign the new children
  • compare the previous and next components
    • previous Child handle
      • if these is the same component and element , move it (MOVE_EXISTING)
      • if these is the same component but not the same element, remove element (REMOVE_NODE)
        • remove event monitor of previous child
      • if a previous child which was not exist in nest queue, delete the component
    • next child handle
      • add the new node (INSERT_MARKUP)

ReactDOMComponent.prototype._patch

  • delete REMOVE_NODE nodes
  • delete MOVE_EXISTING nodes
  • insert INSERT_MARKUP nodes
  • insert MOVE_EXISTING nodes

参考

reactjs源码分析-上篇
reactjs源码分析-下篇(更新机制实现原理)
React为什么要使用Virtual DOM
React虚拟DOM浅析
React 介绍
渲染树构建、布局及绘制
浏览器工作原理
How WebKit Works

iOS布局技术的演变和iPhoneX适配

发表于 2017-11-02 | 分类于 iOS | 阅读次数

Top and Bottom Layouts Guides

iOS11后苹果推出了”Safe Area Layout Guide”布局技术,废弃了iOS7推出的布局技术”Top and Bottom Layouts Guides”,”Top and Bottom Layouts Guides”寿终正寝,让我们回顾一下它彪悍的历史。

借用Harrison的一张图来说明一下”Top and Bottom Layouts Guides”

Apple 引入topLayoutGuide和bottomLayoutGuide做为UIViewController的属性,用来设置你内容区域不要被navigation、status、tabbar遮挡。
上图绿色部分的Content View,加了一个顶部约束相对于topLayoutGuide的底部的锚,加了一个底部约束相对于bottomLayoutGuide的顶部的锚。

Safe Area Layout Guide


safe Area帮助我们将View放到整个屏幕可视的区域。即使navigationbar被设置为透明,系统也认为安全区域是从navigationbar的bottom开始计算的

additionalSafeAreaInsets扩展安全区域
使用additionalSafeAreaInsets可以扩展安全区域使它包括你自定义的content,每个View、controller都可以改变安全区域嵌入的区域

safeAreaInsets
safeAreaInsets表示一个view距离该View的安全区域的边距。一个controller的根视图的safeAreaInsets包括了被statusBar、navigationBar、tabbar等覆盖的区域,同时包括
additionalSafeAreaInsets自定义的insert值。

Positioning Content Relative to the Safe Area

Adapt for Safe Area Layout Guide

tableview下移20pt或64pt

iOS11的automaticallyAdjustsScrollViewInsets被废弃了。在iOS 11中决定tableView的内容与边缘距离的是adjustedContentInset属性,而不是contentInset。
当tableView超出安全区域时系统自动调整了SafeAreaInsets值从而影响adjustedContentInset值。

iPhoneX的适配

iPhoneX设备的top多了刘海,bottom去了home键变成圆角的全屏区域。

竖屏

iPhone8 竖屏

iPhoneX 竖屏

iPhoneX竖屏状态时top的安全区域从20到44,如果带navigationbar则从64到88。bottom的安全区域从0变成34

横屏

iPhone8 横屏

iPhoneX 横屏

iPhoneX横屏状态时top安全区域为0,如果带navigationbar横屏下会缩小top为34,bottom的安全区域也为0,但是left和right都变成44

controls适配

所有的controls都不应该出现在safe area,项目中可能有一些自动布局的代码bottom的约束没有相对于bottomLayoutGuide,由于iPhoneX的底部是圆弧状的,所以控件如果靠边可能会被遮挡。适配的方法是添加约束到layoutMargins。

1
2
3
4
5
let margin = view.layoutMarginsGuide
NSLayoutConstraint.activate([
button.leadingAnchor.constraint(equalTo: margin.leadingAnchor),
button.bottomAnchor.constraint(equalTo: margin.bottomAnchor)
])

滑动手势适配

因为iPhoneX没有Home键,所以开锁是从底部往上滑来进行的。因此在底部34高度的区域,我们不应该放置会被响应的控件

Table View Content Insets适配

iOS11新增了一个insetsContentViewsToSafeArea属性来控制cell的content View是否insert到safe area。默认是true,这会导致在横屏时iPhoneX有一些微妙的变化。

看一下给Cell的content view设置背景色会是什么效果:

iPhone8下的效果:

iPhoneX下的效果:

为什么会有这样的效果,因为Cell的content View是在安全区域里面的。但是cell的backgroundView却扩展到整个屏幕,所以iPhoneX下设置背景色得设置cell的backgroundView而不是contentView的。
设置cell的backgroundView的效果如下

参考

本文参考了相关文献

Supporting iPhone X
Safe Area Layout Guide
iOS 11 安全区域适配总结
你可能需要为你的APP适配iOS11
iOS适配汇总文章

iOS开发性能最佳实践之——如何找到多线程技术合适的应用场景

发表于 2017-10-30 | 分类于 iOS | 阅读次数

我们在衡量某项技术、开源组件的性能时经常会设计一些对比实验来做验证。这种对比实验就像田径比赛,几个人一起赛跑,谁跑得快就是冠军。我们开发的功能可以跑在不同的硬件条件的设备,一般我们会选择条件最差的设备,或者我们业务应该支持的最差设备来做实验。但软件功能所承载的业务场景通常也比较多变,就像一个运动员要跑100米、800米、3000米,设计单一的条件最后做出来的实验结果可能没有太大意义。比如做iOS多线程技术性能的对比实验时,如果这样设计初始条件“控制线程数为20个、1000次线程任务循环次数“,最终得到的实验结果对工程开发的指导意义就不够。不同业务场景条件差别其实很大,比如IM、位置实时更新、抢订单属于高频率、低任务耗时,而数据库等IO操作属于低频率、高任务耗时。所以我们需要归纳各种多线程场景,使用多因素设计,设计多个自变量。还是回到多线程性能实验的例子。自变量:线程数、任务耗时、任务数。自变量的条件要能覆盖常用的业务场景,线程数:5~15~30,任务数:1000~10000~100000,任务耗时:1~5~10毫秒。最终得出来的实验结果也可以较准确得命中各种实际使用场景。

iOS常用的多线程技术有NSThread、GCD、pthread、NSOperation几种技术。之前看过一些开发者对这几种技术做过实验测试,得出具体的数据比较。但仅仅统计一定数量级别的运行效率对实际应用没有太大的指导性意义,最好是能模拟实际场景的线程环境来做测试。

那不同场景的因子有什么呢?
并发频率高低、任务耗时长短、稳定性

对应的测试关注点是什么?
并发频率高低:统计多个线程并发运行,最后全部运行完成的耗时长短
任务耗时长短:统计单个线程里不同耗时任务的完成时间
稳定性:进行多次实验,统计数据的变动区间

分析出上面测试关注点后,我们可以开始来设计实验了。

实验对象:
NSThread、单队列并行GCD、多队列串行GCD、NSOperation、pthread

实验环境:
设备、编译环境

实验变量:
线程的数量、每个线程里执行的任务次数、每个任务的耗时长短

实验方法:
模拟实际场景,设置几组实验变量的搭配,比如下面几组。

线程数:5/15个
任务数:1000/10000/100000次
耗时:1/5/10 毫秒

实验结果:

任务平均耗时速度排名
NSOperation > 多队列串行GCD/NSThread > 单队列并行GCD

所有线程任务全部完成速度排名

多队列串行GCD/NSThread > NSOperation/单队列并行GCD

实验结论

多队列串行GCD和NSThread的特点相似,多个线程高并发的完成速度最快,单个线程的执行性能中等。
所以这两种技术适合高频率、低任务耗时的应用场景,比如IM、位置更新、抢订单等实时消息通信场景

NSOperation的特点是单个线程的执行性能最快,所以适合低频率、高任务耗时的应用场景,比如数据库、文件等IO场景

计算机的内存分配问题

发表于 2017-10-28 | 分类于 iOS | 阅读次数

内存对齐原则

什么是内存对齐
计算机的内存都是按byte来划分的,计算机在范围不同数据类型变量时会按照特定的内存地址访问,所以各种数据类型会按照一定规则在内存空间上排列,而不是顺序地一个一个排列,这就是内存对齐

有什么用
各平台对数据类型的读取会按特定的地址开始存取,访问不对齐的内存处理器需要做两次处理,访问对齐的内存处理器只需要做一次处理

对齐算法

1 数据类型以自身需要占用的字节数做为内存对齐值。

对于char型数据,其自身对齐值为1,对于short型为2,对于int,float型,其自身对齐值为4,对于double型,其自身对齐值为8,单位字节。

例如:int变量所分配的内存地址必须能够被4整除,double则必须能倍8整除

2 结构体或类自身的对齐值和自身成员变量对齐值最大的那个保持一致。

1
2
3
4
5
6
7
例如:
struct D {
char a;
int b;
double c;
}

结构D的成员变量里c的对齐值最大为8,所以结构D的对齐值就是8

3 指定对齐值,使用#pragma pack (value)可以指定对齐值为Value

1
2
3
4
5
6
7
8
例如:
#pragma pack (1) // 指定对齐值为1
struct D {
char a;
int b;
double c;
}
#pragma pack () // 取消指定对齐,恢复默认的对齐方式

4 当数据成员、结构体、类处于指定对齐区间时,真实的有效对齐值是自身对齐值和指定对齐值两个中更小的那个。

内存对齐百科

一个多层嵌套URL引发的URLEncode血案

发表于 2017-10-25 | 分类于 iOS | 阅读次数

URL里中文字符正确过滤姿势

今天有个同事跟我反馈,有个复杂参数的路由跳转需求,目前路由解析满足不了。
这个需求是服务推送一个URL scheme到app,又app的路由寻找到对应的页面。
这个URL结构大致如下:

1
blacktea://gotoWebView?url=https://m.alibaba.com?key1=value1&url2=https://www.blacktea.com.js&key3=value3

第一层嵌了一个参数url,第二层url的value里又嵌了一个参数url2。

服务端对这个url做了encode,推送到app的字符串如下:

1
blacktea://gotoWebView?url=https%3a%2f%2fm.alibaba.com%3fkey1%3dvalue1%26url2%3dhttps%253a%252f%252fwww.blacktea.com.js%26key3%3dvalue3

在路由里为了避免空格和中文字符,会对传入的url分别调用stringByReplacingPercentEscapesUsingEncoding和stringByAddingPercentEscapesUsingEncoding

1
2
urlString = [urlString stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
urlString = [urlString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];

问题来了,因为stringByReplacingPercentEscapesUsingEncoding会对”%3a%2f%3d%26%3f”等被转义的保留字符反转义回”:/=&?”。
而stringByAddingPercentEscapesUsingEncoding只会对中文非法字符做percent转义,不会处理”:/=&?”等保留字符做encode处理。所以最后经过路由后,URL变成如下:

1
blacktea://gotoWebView?url=https://m.alibaba.com?key1=value1&url2=https%253a%252f%252fwww.blacktea.com.js&key3=value3

这就导致第一层的参数url里的链接没有被encode,url2、key3这两个参数都被错认为是第一层的参数了。

路由层做过滤的实际意图是希望,把中文字符转义掉,同时又不影响url各层参数的encode状态。
解决的办法是使用如下两个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static NSString *ASCRouterReplacingPercentEscapes(NSString *string, NSStringEncoding encoding){
NSString *result = ( NSString *)CFBridgingRelease(CFURLCreateStringByReplacingPercentEscapesUsingEncoding(kCFAllocatorDefault,
(__bridge CFStringRef)string,
NULL,
CFStringConvertNSStringEncodingToEncoding(encoding)));
return result;
}
static NSString *ASCRouterAddingPercentEscapes(NSString *string, NSStringEncoding encoding) {
return (NSString *)CFBridgingRelease(CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault,
(__bridge CFStringRef)string,
NULL,
NULL,
CFStringConvertNSStringEncodingToEncoding(encoding)));
}

使用以上2个方法,便只会对中文非法字符进行转义,不会影响其他保留字符的转义情况。

多层嵌套url正确的encode姿势

除此之外,对于多层嵌套的url需要正确地encode.

1
blacktea://gotoWebView?url=https://m.alibaba.com?key1=value1&url2=https%253a%252f%252fwww.blacktea.com.js&key3=value3

对于上面例子这个url,需要做2层的urlencode

错误encode方式:

blacktea%3a%2f%2fgotoWebView%3furl%3dhttps%3a%2f%2fm.alibaba.com%3fkey1%3dvalue1%26url2%3dhttps%3a%2f%2fwww.blacktea.com.js%26key3%3dvalue3

正确encode方式:

第一次encode
from:
https://www.blacktea.com.js
to:
https%3a%2f%2fwww.blacktea.com.js

第二次encode

from:
https://m.alibaba.com?key1=value1&url2=https%3a%2f%2fwww.blacktea.com.js&key3=value3

to:
https%3a%2f%2fm.alibaba.com%3fkey1%3dvalue1%26url2%3dhttps%253a%252f%252fwww.blacktea.com.js%26key3%3dvalue3

result:

1
blacktea://gotoWebView?url=https%3a%2f%2fm.alibaba.com%3fkey1%3dvalue1%26url2%3dhttps%253a%252f%252fwww.blacktea.com.js%26key3%3dvalue3

客户端拿到这个被多层encode的url后,再一层一层能地解开,没解开一层都要decode一遍。

UrlEncode编码原理

编码原理:
将需要转码的字符转为16进制,然后从右到左,取4位(不足4位直接处理),每2位做一位,前面加上%,编码成%XY格式。

比如:
空格ASCII码是32,对应16进制是20,那么urlencode编码结果是:%20
比如:
中ASCII码是-10544,对应的16进制是FFFFFFFFFFFFD6D0,那么urlencode编码结果是:%D6%D0

URI统一资源标识符里允许的所有字符里,分为保留字符和非保留字符。百分号编码(Percent-encoding)会把保留字符转义为特殊字符序列。

百度百科urlencode

12…4
Blacktea

Blacktea

iOS music footbood barcelona 歌手、程序员、爵士乐、巴萨罗那、自由主义者、学生

38 日志
14 分类
35 标签
RSS
GitHub 微博 豆瓣 知乎
© 2017 - 2019 Blacktea
由 Hexo 强力驱动
主题 - NexT.Pisces