Gone With the Wind

double值失精之后的表现

之前在这篇文章中解释了0.1+0.2为什么不等于0.3而等于0.30000000000000004的原因,根源问题还在于存储精度的问题,当时只是简单觉得丢失精度后的行为是理所当然的,并没有丢失精度后是如何处理的,下面就来分析一下:

精度丢失加法

首先,JS,或者说double类型能精确表达的最大的数是Math.pow(2, 53),那么下面表达式的值你肯定也知道了

1
2
Math.pow(2, 53) + 1 === 9007199254740992
Math.pow(2, 53) + 2 === 9007199254740994

那么下面表达式的值呢:

1
Math.pow(2, 53) + 3 ?

可能一开始你会想当然得认为上面的值是:9007199254740994,因为精度不到,所以就把这位丢弃,然而,确切的结果是:

1
9007199254740996

精度丢失算法

显然上面的运算都是在精度丢失的场景下进行的,那么,如何保证精度尽量正确就是必须要做的事,

0舍1进

舍0进1类似4舍5入,0就舍弃,1就向前进一位,举个例子

Math.pow(2, 53) - 1在内存中存储方式为:

1
0|10000110011|1111111111111111111111111111111111111111111111111111

之前我们说到有这么一个公式(注意,这里是二进制运算,那个2的N次方可以理解为十进制的10的N次方):

带入上述计算公式:

1
2
3
符号位 s = 0
指数位 E = 52
整数位 M = 1.1111111111111111111111111111111111111111111111111111

所以最终结果就是M的小数点往右移动52位的结果。

Math.pow(2, 53)在内存中存储方式为:

1
0|10000110100|0000000000000000000000000000000000000000000000000000
1
2
3
符号位 s = 0
指数位 E = 1023 + 52 + 1 = 1077
整数位 M = `1.0000000000000000000000000000000000000000000000000000`

最终结果为M往右移动53位

Math.pow(2, 53) + 1在内存中存储方式为:

1
0|10000110100|0000000000000000000000000000000000000000000000000000

可以看到和Math.pow(2, 53)一致

Math.pow(2, 53) + 2在内存中存储方式为:

1
0|10000110100|0000000000000000000000000000000000000000000000000001

可以看到最后一位多了一个1

1
2
3
符号位 s = 0
指数位 E = 53
整数位 M = 1.0000000000000000000000000000000000000000000000000001

最终结果也就是M往右移动53位,变成了10000000000000000000000000000000000000000000000000001|0

因为右边只有52位,而我们的指数位有53位,那么最后一位就变成0了,也就是竖线后面的那一位,所以这也是精度丢失的根本原因。

Math.pow(2, 53) + 3在内存中存储方式为:

1
0|10000110100|0000000000000000000000000000000000000000000000000010
1
2
3
符号位 s = 0
指数位 E = 53
整数位 M = 1.0000000000000000000000000000000000000000000000000010

最终结果:10000000000000000000000000000000000000000000000000010|0

结论

Math.pow(2, 53)的整数位:0000000000000000000000000000000000000000000000000000|0

后面那个|符号我们可以认为是小数点,来理解0舍1进

  1. [a]当Math.pow(2, 53) + 1时,变成..000|1,1的前值为0,舍弃

  2. [b]当Math.pow(2, 53) + 2时,变成..001|0,舍弃尾部,值精确

  3. [c]当Math.pow(2, 53) + 3时,变成..001|1,1的前值为1,舍0进1,变成,..010|0

  4. [c]当Math.pow(2, 53) + 4时,变成..010|0,舍弃尾部,值精确
  5. [c]当Math.pow(2, 53) + 5时,变成..010|1,1的前值为0,舍弃

  6. [d]当Math.pow(2, 53) + 6时,变成..011|0,舍弃尾部,值精确

  7. [e]当Math.pow(2, 53) + 7时,变成..011|1,1的前值为1,舍0进1,变成, ..100|0

  8. [e]当Math.pow(2, 53) + 8时,变成..100|0,舍弃尾部,值精确
  9. [e]当Math.pow(2, 53) + 9时,变成..100|1,1的前值为0,舍弃

  10. [f]当Math.pow(2, 53) + 10时,变成..101|0,舍弃尾部,值精确

  11. [g]当Math.pow(2, 53) + 11时,变成..101|1,1的前值为1,舍0进1,变成, ..110|0

  12. [g]当Math.pow(2, 53) + 12时,变成..110|0,舍弃尾部,值精确
  13. [g]当Math.pow(2, 53) + 13时,变成..110|1,1的前值为0,舍弃

  14. [h]当Math.pow(2, 53) + 14时,变成..111|0,舍弃尾部,值精确

  15. [h]当Math.pow(2, 53) + 15时,变成..111|1,1的前值为1,舍0进1,变成, .1000|0

  16. [h]当Math.pow(2, 53) + 16时,变成..1000|0,舍弃尾部,值精确
  17. [h]当Math.pow(2, 53) + 17时,变成..1000|1,1的前值为0,舍弃

  18. [i]当Math.pow(2, 53) + 18时,变成..1001|0,舍弃尾部,值精确

  19. [j]当Math.pow(2, 53) + 19时,变成..1001|1,1的前值为1,舍0进1,变成, .1010|0

  20. [j]当Math.pow(2, 53) + 20时,变成..1010|0,舍弃尾部,值精确

好了,列举了20个,你大概也看出规律了,也看明白了,就不多列了。

恒1

恒1法总得来说就是在舍去的尾部总是置位1,计算方法比较简单,可以自行上网搜索。

记一次Node和Go的性能测试

以前简单测过go的性能,高并发场景下确实比node会好一些,一直想找个时间系统性地测一下,手头正好有一台前段时间买的游戏主机,装了ubuntu就开测了

准备工作

  1. 测试机和试压机系统都是ubuntu 18.04.1
  2. 首先安装node和go,版本分别如下:

    node 10.13.0

    go 1.11

  3. 测试机和试压机修改fd的限制​ulimit -n 100000​,否则fd很快就用完了。

  4. 如果是试压机是单机,并且QPS非常高的时候,也许你会经常见到试压机有N多的连接都是​TIME_WAIT​状态,具体原因可以在网上搜一下,执行以下命令即可:

    1
    2
    3
    $ sysctl -w net.ipv4.tcp_timestamps=1
    $ sysctl -w net.ipv4.tcp_tw_reuse=1
    $ sysctl -w net.ipv4.tcp_tw_recycle=1
  5. 测试工具我用的是siege,版本是​3.0.8​。

  6. 测试的js代码和go代码分别如下:

Node(官网的cluster示例代码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;

if (cluster.isMaster) {
console.log(`Master ${process.pid} is running`);

// Fork workers.
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}

cluster.on('exit', (worker, code, signal) => {
console.log(`worker ${worker.process.pid} died`);
});} else {
// Workers can share any TCP connection
// In this case it is an HTTP server
http.createServer((req, res) => {
res.end('hello world\n');
}).listen(8000);

console.log(`Worker ${process.pid} started`);}

Go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package main

import(
"net/http"
"fmt"
)

func hello(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "hello world")
}

func main() {
http.HandleFunc("/", hello);
err := http.ListenAndServe(":8000", nil);
if err != nil {

}
}

开始测试

首先开始并发量的测试,然而。。。游戏主机的CPU是i7-8700K,性能太强,以至于拿了两台mac也没能压满。。。

四处寻找一番,找到了好几年前花了千把块钱配的nas,上去一看是颗双核的i3-4170,妥了,这下肯定没问题

正式开始

跳过了小插曲,直接开测

I/O密集型场景

  • Node - 多进程模型,可以看到因为所有请求都由master进程转发,master进程成为了瓶颈,在CPU占用100%的情况下,worker进程仅仅只占了50%,因此整体CPU利用率只有70%。
  • qps: 6700


  • Go - 单进程多线程模型,系统剩余CPU差不多也有30%,查了一下原因是因为网卡已经被打满了,千兆网卡看了下已经扎扎实实被打满了。
  • qps: 37000

千兆网卡已经被打满了


在helloworld场景下,如果我们有万兆网卡,那么go的qps就是50000,go是node的7倍多,乍一看这个结果就非常有意思了,下面我们分析下原因:

  1. 首先node官方给的cluster的例子去跑压测是有问题的,CPU物理核心双核,超线程成4核,姑且我们就认为有4核。
  2. cluster的方案,先起主进程,然后再起跟CPU数量一样的子进程,所以现在总共5个进程,4个核心,就会造成上下文频繁切换,QPS异常低下
  3. 基于以上结论,我减少了一个worker进程的数量
  • Node 多进程模型下,1个主进程,3个worker进程
  • qps: 15000 (直接翻倍)


以上的结果证明我们的想法是对的,并且这个场景下CPU利用率几乎达到了100%,不是很稳定,暂且我们可以认为压满了,所以我们可以认为1master4worker场景下,就是因为进程上下文频繁切换导致的qps低下

那么进一步想,如果把进程和CPU绑定,是不是可以进一步提高qps?

  • Node 多进程模型,并且用​taskset​命令把进程和CPU绑定了
  • qps: 17000 (比不绑定cpu性能提高了10%多)


结论 :node在把CPU压满的情况下,最高qps为:17000,而go在cpu利用率剩余30%的情况,qps已经达到了37000,如果网卡允许,那么go理论上可以达到50000左右的qps,是node的2.9倍左右。

CPU密集场景

为了模拟CPU密集场景,并保证两边场景一致,我在node和go中,分别添加了一段如下代码,每次请求循环100W次,然后相加:

1
2
3
4
var b int
for a := 0; a < 1000000; a ++ {
b = b + a
}

  • Node 多进程模型:这里我的测试方式是开了4个worker,因为在CPU密集场景下,瓶颈往往在woker进程,并且将4个进程分别和4个核绑定,master进程就让他随风飘摇吧
  • qps: 3000


  • go:go不用做特殊处理,不用感知进程数量,不用绑定cpu,改了代码直接走起
  • qps: 6700,依然是node的两倍多

结论

Node因为用了V8,从而继承了单进程单线程的特性,单进程单线程好处是逻辑简单,什么锁,什么信号量,什么同步,都是浮云,老夫都是await一把梭。

而V8因为最初是使用在浏览器中,各种设置放在node上看起来就不是非常合理,比如最大使用内存在64位下才1.4G,虽然使用buffer可以避开这个问题,但始终是有这种限制,而且单线程的设计(磁盘I/0是多线程实现),注定了在如今的多核场景下必定要开多个进程,而多个进程又会带来进程间通信问题。

Go不是很熟,但是前段时间小玩过几次,挺有意思的,一直听闻Go性能好,今天简单测了下果然不错,但是包管理和错误处理方式实在是让我有点不爽。

总的来说在单机单应用的场景下,Go的性能总体在Node两倍左右,微服务场景下,Go也能起多进程,不过相比在机制上就没那么大优势了。

Node

优点

  1. 单进程单线程,逻辑清晰
  2. 多进程间环境隔离,单个进程down掉不会影响其他进程
  3. 开发语言受众广,上手易

缺点

  1. 多进程模型下,注定对于多核的利用会比较复杂,需要针对不同场景(cpu密集或者I/O密集)来设计程序
  2. 语言本身因为历史遗留问题,存在较多的坑。

Go

优点

  1. 单进程多线程,单个进程即可利用N核
  2. 语言较为成熟,在语言层面就可以规避掉一些问题。
  3. 单从结果上来看,性能确实比node好。

缺点

  1. 包管理方案并不成熟,相对于npm来说,简直被按在地上摩擦
  2. if err != nil ….
  3. 多线程语言无法避开同步,锁,信号量等概念,如果对于锁等处理不当,会使性能大大降低,甚至死锁等。
  4. 本身就一个进程,稍有不慎,进程down了就玩完了。

一段有意思的Nodejs异步处理代码

首先看下这段代码的输出是什么:

1
2
3
4
5
6
7
8
9
10
11
setTimeout( () => {
console.log( 1 );
}, 1 );

setImmediate( () => {
console.log( 2 );
} );

process.nextTick( () => {
console.log( 3 );
} );

我猜大概大部分人的电脑上的输出结果是:3 1 2,并且无论你执行多少次,结果永远是3 1 2

再看看下一段代码的执行结果:

1
2
3
4
5
6
7
setTimeout( () => {
console.log( 1 );
}, 1 );

setImmediate( () => {
console.log( 2 );
} );

其实就是把process.nextTick调用去掉了,这时候你会发现输出开始变得较为随机:1 2或者2 1

那么第一段代码哪些人的电脑输出结果会不一样?土豪!没错就是土豪,土豪不仅仅在生活上碾压你,在代码上甚至都是如此。

NodeJs的EventLoop

有关NodeJs的EventLoop不多说了,网上讲解文章非常多,官网就有一篇讲得非常好也通俗易懂的文章:https://nodejs.org/en/docs/guides/event-loop-timers-and-nexttick/

看完上面的文章也许你对第二段代码的随机输出明白了,但是第一段为何始终都是3 1 2呢?

为什么是3 1 2?

也许你会觉得结果应该会是在3 2 13 1 2之间随机,但是你可以在你自己的电脑上面试试看,基本永远都是3 1 2,那么为什么呢?

上面的文章只介绍了事件循环相关的内容,但是对于nextTick以及整个NodeJs执行流程的介绍并不多,这里自己通过查看源码发现以下流程:

  1. NodeJs启动阶段

    • 准备Node环境
    • 执行代码
    • 检查nextTickQueue,如果有就执行。
  2. 开始事件循环,这一步一定是在启动阶段之后,因为事件循环的前提是得有事件触发,事件触发的前提就是执行了我们的代码,如果没有触发事件,那么Node进程就退出了。

    • timer
    • I/O

看到这里,就已经能非常好地解释上面的代码了:

  1. 首先我们的主程序执行完,nextTickQueue中有一个回调,首先执行这个回调,打印一个console.log

  2. 然后开始事件循环,执行TimerI/O等阶段,然而因为我们设置的timer的超时时间是1毫秒,而我们在nextTick里面的回调的console.log还是比较耗时间的,到了Timer阶段,1毫秒早就过去了,所以执行结果永远都是3 1 2

其实我测试了一下,我的电脑设置为setTimeout( ..., 4 ),4毫秒的延迟后,就会开始出现随机结果,也就是一次console.log差不多花费3毫秒的时间,相当耗时啊,并且在process.nextTick回调里面把console.log去掉后,开始出现随机1 2或者2 1的结果。

为什么土豪的执行结果可能不是3 1 2呢?土豪的电脑配置高,一个console.log也许花不了几微秒,Timer阶段检查1毫秒没到,就往下执行了,之后就到了check阶段,执行setImmediate的回调,输出就变成了3 2 1

所以,努力奋斗吧,争取买得起一台输出结果是3 2 1的电脑,哈哈。

强类型?弱类型?强类型!

“灵活”的JavaScript

以前我很喜欢JavaScript,并不仅仅是因为他是我吃饭的工具,更多因为他非常灵活,从学校了写多了C++或者Java,遇到各种诡异的编译链接等等的错误,到JavaScript弱类型动态语言,简直就是满地撒欢。

我可以使用各种奇淫巧技,虽然比不上Python,也足够让我用1行代码实现别人3行代码的功能,当时我以同样的功能下代码字符越少越牛逼为荣。

然而慢慢地,别人开始接手我写的代码,包括自己开始看以前自己写的代码,虽然别人有三行代码,但是一眼就看明白了三行代码所表达的意思,而我的一行代码,看了好几分钟才能看明白所表达的意思,有时候还不一定正确,也许需要跑起来才能知道效果,我开始质疑自己的这种行为,比如以下行为:

  • ~~n, 效果等同于Math.floor( n ),虽然运行更快,但是一眼并不能看出什么意思,而函数Math.floor一眼就能看出什么意思。
  • n === n,效果等同于isNaN( n ),因为NaN并不等于他自己。
  • (![]+[])[+[]]+(![]+[])[+!+[]]+([![]]+[][[]])[+!+[]+[+[]]]+(![]+[])[!+[]+!+[]]甚至包括这种,我还真写过,只是没有这么夸张。

灵活的代价

下面的代码是一个真实的例子,我调用了别人的代码,他的函数:

1
2
3
4
5
6
function test() {
...

// 这里返回是一个Promise
return doSomeAsync();
}

返回的是一个Promise,所以我用await去调用,到这里并没有问题。

而出现问题来源于一次需求的增加,在执行这种函数时,需要判断一些意外情况,如果出现意外,就弹出一个对话框,让用户点击确认后才继续执行,于是他的代码变成了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

function test( needConfirm ) {

...

if ( needConfirm ) {
Dialog.show( {
content: '您确定要继续执行么?',
onOk: () => {
doSomeAsync();
}
} );
return;
}

// 这里返回是一个Promise
return doSomeAsync();

}

这么改动乍一看也没什么问题,如果需要确认,就弹出一个对话框,给用户确认,当用户点击确认的时候,在回调函数里面执行原本的操作。

而对于我的代码来说,我依赖于doSomeAsync执行的结束,所以,当我任然使用await test( true )去调用时,我的代码就报错了,因为true === needConfirm的时候返回的是undefinedawait并不会等待,而是继续执行下面的代码,所以导致了错误的出现。

“束缚”的TypeScript

相信以上的代码绝大部分人都写过,我们再仔细看下,其实上面这段代码的问题在于函数的行为不一致,正常情况是返回一个Promise,而分支情况是返回一个undefined,我们看看在TypeScript下怎么写这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function test( needConfirm: boolean ): Promise<void> {

...

if ( true === needConfirm ) {
Dialog.show( {
content: '您确认要继续执行么?',
onOk: () => {
doSomeAsync();
}
} );
return;
}

return doSomeAsync();
}

如果你任然这么写,那么TypeScript的编译阶段就会报错,因为你规定了函数需要返回一个Promise,但是却存在返回undefined的情况,你不得不去修改你的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

functon test( needConfirm: boolean ): Promise<void> {

...

if ( true === needConfirm ) {

let resolveFunc = null;

Dialog.show( {
content: '您确定要继续执行么?',
onOk: async function(): Promise<void> {
const result = await doSomeAsync();
resolveFunc( result );
}
} );

return new Promise( ( resolve, reject ) => {
resolveFunc = resolve;
} );
}
return doSomeAsync();
}

编译通过,函数行为一致,调用者也无需感知里面的逻辑。

最后

强类型写起来限制会更多,然而对于代码的可维护性,可运行性都有很大的提升。

现在前端的复杂性提升地非常快,几年前也许前端仅仅只是更多作为展示型页面存在,但是现在的前端已经承载了太多的东西,用户的交互也发生了很大变化,JavaScript也可以写ReactNative的APP,可以写NodeJs服务,可以用NodeWebkit等工具写跨平台的客户端,使用TypeScript严格按照强类型语言去写你的应用,让你的应用具备更高的可读性和可维护性。

当然事事无绝对,还是得看你的场景,杀鸡焉用牛刀,JavaScript自有存在的意义,至少是目前浏览器官方语言,当你的应用重逻辑时,放手用`TypeScript吧。

在typescript中使用delegate来进行解耦

无处不在的耦合

通常你写的一个React组件,如果需要和别的组件发生交互,那么你会怎么做?

1
2
3
4
5
6
7
8
9
public componentDidMount(): void {
this.refs.mycom.test();
}

public render() {
return (
<MyCom ref="mycom" />
);
}

这种做法相对来说比较普遍,也比较合情合理,那么如果MyCom组件执行了什么操作之后需要通知到父组件呢?

1
2
3
4
5
6
7
8
9
public onNotify(): void {

}

public render() {
return (
<MyCom notify={ this.onNotify.bind( this ) } ref="mycom" />
);
}

很多时候我们会选择传进去一个回调函数,当MyCom需要发送通知的时候再调用props.notify

虽然这种方式能实现我们的需求,但也仅仅是实现而已,你会发现这种方式并不是很优雅,而且当你的回调函数多了之后,那简直就是噩梦,后来来使用这个组件的人完全不知道需要传哪些回调函数进去,也不知道哪些回调是可选的,哪些是必选的。

protocol

ProtocolOc里面的概念,类似Java里面的Interface,和TypeScript中的Interface也比较类似,其中有一个用处就是用于两个类之间的通信解耦,而delegate更多的是一种约定俗成的规则,看看具体怎么用:

首先我有一个A类,用

A.h

1
2
3
4
5
6
7
8
9
@protocol ADelegate
- (void) methodFromA;
@end

@interface A : NSObject
// 声明委托变量
@property (assign, nonatomic) id<ADelegate> delegate;
- (void) test;
@end

A.m

1
2
3
4

// 在A类的实现文件里面,在需要进行回调的时候,只需要调用当前实例的delegate对象,而不用理会到底是谁调用了自己,而且对方肯定实现了delegate指向的protocol
// 需要的时候调用
[self.delegate methodFromA];

此时,我有一个B类,需要调用A类,同时需要监听回调函数:

B.h

1
2
3
4
5
6
#import "A.h"

// 这里实现了Adelegate Protocol
@interface B : NSObject <ADelegate>
...
@end

B.m

1
2
3
4
5
6
7
8
9
10
11
- (void) someMethod {
A *aInstance = [[A alloc] init];
// 这里把当前实例赋值给A类实例的delegate属性
aInstance.delegate = self;
}

// 实现了ADelegate
- (void) methodFromA {
当A类调用self.delegate.methodFromA时,就可以在这里执行你自己的代码了
// ...
}

TypeScript中的delegate

上面可以看到其实delegate的使用非常简单,只是一种约定的规则,同时delegate其实也是一种设计模式,在JS中其实也可以使用,只是JS是弱类型语言,无法通过编译器来约束一些行为,所以依赖于TypeScript的强类型,我们也可以来使用一下delegate

组件A

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export interface ADelegate {
methodFromA();
}

export class A extends React.Component<any, any> {
public delegate: ADelegate;

public render() {

// 调用delegate的方法
this.delegate.methodFromA();

return (
<div></div>
);

}

}

组件B

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { A, ADelegate } from 'A';

// 这里实现了Interface: ADelegate
export class B extends React.Component<any, any> implements ADelegate {
// 从A实现的回调。
public methodFromA(): void {

}

public componentDidMount(): void {
const aCom: A = <A>this.refs.acom;
aCom.delegate = this;
}

public render() {
return (
<A ref="acom" />
);
}
}

效果

上面的代码可以看到,A组件面向的不是调用他的组件,而是他自己声明的delegate,完全无需关心是谁在调用自己,在什么场景下调用自己

而B组件也完全不用关心A组件的具体实现,只要实现A组件提供的Interface:ADelegate,从中选择自己需要的方法即可,两者实现解耦,面向delegate编程。

webpack的chunk id和cdn缓存之间的爱恨情仇

之前项目中引入了一个插件,把webpack的chunk id使用md5代替,使得其无序化,开发环境没有问题,但是在配合CDN使用的时候,却发现每次发布新版本之后,页面直接报错打不开,提示加载某某chunk的时候无法找到,清除缓存之后问题解决,然而把id无序化插件去掉之后就没问题了,看来问题和缓存和chunk id有关。

起因

webpack chunk

先说说webpack的chunk是怎么一回事,如下图:

有四个项目文件,分别为:file1.js, file2.js, file3.js, file4.js.

1
2
3
4
5
file1.js
file2.js

file3.js
file4.js

其中,file1依赖了file2file3依赖了file4,在webpack里面,通常情况下一个文件就是一个module,有时候也有一个文件多个module的情况,不过不多见,webpack会给每一个module分配一个id,这个id是自增的:

1
2
3
4
5
file1.js -> module1, id: 1
file2.js -> module2, id: 2

file3.js -> module3, id: 3
file4.js -> module4, id: 4

因为file1file2对于file3file4都没有什么依赖关系,所以webpack把这四个文件分成了两个chunk,同时分别给了一个id,也是自增的:

1
2
3
4
5
6
7
chunkA: ChunkId1
-> module1
-> module2

chunkB: ChunkId2
-> module3
-> module4

一个chunk就对应于webpack打包之后的文件,于是,打包之后我们现在有两个文件:

1
2
3
4
5
6
7
8
9
10

file1.js
filea.hash.js
file2.js

----------------------------------

file3.js
fileb.hash.js
file4.js

两个chunk所对应的打包后的文件名为: ${fileName}.${md5}.js.

CDN缓存

项目中使用了覆盖式的CDN缓存方式,也就是不同发布之间没有版本概念,而是直接覆盖原来的文件,因为文件名中有MD5值,所以只要文件内容改变,那么文件名也就改变。

好处是如果新版本没有变更的文件,可以让浏览器复用之前的缓存,达到最小更新目的。

问题出现

原本这一切都运行得不错,直到后来在项目中加入了一个webpack插件,用来做chunk id的无序化,至于加入这个插件的原因,是因为webpack自带的chunk id的生成规则:

webpack chunk id生成方式

webpack的chunk id是从1开始自增的,具体有自己的规则,比如根据引用顺序来给chunk id给定一个id,文件内容的改变并不会影响id,只要引用顺序不变,那么即使文件内容如何改变,都不会影响chunk id,多次打包之后的值都是一样的。

chunk id无序化

上面说chunk id从1开始自增,也就是不同项目之间,chunk id可能都是一样的,这就导致跨项目间调用chunk变得困难了,为了解决这个问题,就引入了一个插件,可以让chunk id无序化,实质上就是用MD5值来代替了chunk id。

为何在CDN场景下发布新版本会导致找不到chunk呢?

把chunk id用MD5值代替,看起来确实没有问题,在版本化的CDN场景下使用或者说在本地开发环境中使用确实是没有问题的,问题出在和覆盖式的CDN同时使用时候的问题。

问题根源

首先我们看chunka和chunkb打包之后的代码:

chunka:

1
2
3
...
__webpack__require.e( '1' ).then( '....' );
...

chunkb:

1
2
3
webpackJsonp([1],{
...
});

chunka中代码,打包前的代码为:

1
import( './file3' );

chunka中需要引用id为1的chunk,然后chunkb的代码中,首先用webpackJsonp方法定义了自己的chunk id为:1,这样,整个代码就联系起来了,也就能正常运行了。

MD5替代chunk id

如果使用MD5来替换原先的chunk id呢?

chunka:

1
2
3
...
__webpack__require.e( 'md51' ).then( '....' );
...

chunkb:

1
2
3
webpackJsonp(['md51'],{
...
});

看起来也没什么问题,反正只要引用的chunk id和声明的chunk id对应上就可以了。尝试修改了一些file3的内容,也就是chunkb的内容有了变更之后:

chunka:

1
2
3
...
__webpack__require.e( 'md52' ).then( '....' );
...

chunkb:

1
2
3
webpackJsonp(['md52'],{
...
});

chunkb的chunk id变成了: md52,同时引用和声明的chunk id都能对应上,运行没问题,那么问题出在哪里呢?我们看看webpack生成文件的文件名:

1
${fileName}.${md5}.js

因为文件名中有MD5值,所以当修改了chunkb之后,那么chunkb对应的文件名肯定就变更了,这个没问题,而chunka其实内容并没有变更,所以chunka的md5没有变化,也就是说chunka的文件名不变。

然而在引入chunk id无序化插件之前,chunkb的内容变化了,chunk id是不会变的,所以chunk b变化之后,打包后的chunka的文件名和内容都是没有变化的,使用浏览器缓存也不会有任何问题。

缓存带来的问题

chunk id变成MD5之后,chunkb的改变,会导致chunka的文件名没有变,然而chuna的代码其实是变化了的,因为引用chunkb的时候是要填写chunkb的chunk id的,因为chunk b的内容改变,其chunk id也跟着改变,浏览器根据文件名缓存,因为文件名没有变化,所以一直读取的都是以前的文件,也就导致了找不到某某chunk的根源,以前因为内容变化了,chunk id是不会变化的,所以使用并没有问题。

webpack MD5生成

也许你会说chunka的内容其实是变化了,为啥md5没有改变?其实一开始我也是这么想的,后来看过webpack代码之后就知道原因了:

上面是生成hash的事件,而替换代码则远远在这之后,其实之前也猜到个大概,从业务上讲其实chunka的内容并没有任何变更,hash也是不应该改变的。

最近对于懒加载的一些思考

记得我刚毕业那会,前端还是jQuery的天下,没有什么项目是一个jQuery搞不定的,如果有,那就再加个插件。

后来慢慢出现了一些前端模块化工具,直至seajs的出现,把模块化推向了一个高潮,期间听说了gmail前端有44万行的JS代码,也是非常震惊。

现在的前端为了更好的用户体验,更快地适应业务,逻辑正在变得越来越复杂,同时也带来越来越多的问题。

体积臃肿的前端

使用webpack的同学对下面这张图很熟悉了,一般我们用webpack打出来的包,都会包含一大堆东西,node_modules文件内容甚至能占到90%以上。

冰山效应

很多时候,用户打开一个页面,同时也加载了很多在这个页面上并不会运行的代码,甚至是永远都不会运行的代码,页面上的这些代码就像海洋里的冰山,运行的只是海平面上很小一部分,很大一部分在海平面下的代码,仅仅只会拖慢我们首页的打开时间。

按需加载

从有模块化起,貌似就有了按需加载功能,很多工具都提供了按需加载的功能,以webpack为例:

1
2
3
const jQuery = await import( 'jQuery' );
 
const model = await import( './model/data.js' );

webpack内置函数import很容易就可以实现按需加载的功能,看起来似乎问题就已经解了?然而这只是问题的第一步,仅仅有了按需加载的能力,问题也会随之而来。

预加载

按需加载必然会带来一次网络请求,假设一个场景:

1
用户点击一个按钮,然后跳出一个弹框。

普通情况下,点击按钮之后发生的事情如下:

1
点击按钮(ms) -> 渲染弹框(ms)

如果我们把弹框做按需加载,那么点击按钮之后发生的事情如下:

1
点击按钮(ms) -> 加载弹框代码(s) -> 渲染弹框(ms)

中间多了一个步骤,并且是按秒计算的从网络加载代码,点击一个按钮之后需要等待一段时间,才会弹出弹框,用户操作非常不流畅,如果存在大量这种场景,体验或许还不如不用按需加载。

有什么办法能解这个问题么?一种方案是:预加载,请看下图:

预加载步骤

  1. 首先页面上收集用户的点击行为日志。
  2. 拿到点击行为日志,得出用户最常用的操作路径,这部分数据在静态打包的时候注入到前端。
  3. 每个人的操作路径会有区别,这里可以在前端也记录一份当前用户的点击数据,这里很多时候虽然本地都已经有缓存了,不过提前把缓存加载到内存,也可以节省很多时间。
  4. 在浏览器空闲时,按照第二步和第三步的数据计算权重,最终决定加载顺序。
  5. 同时,前端可以根据鼠标运动轨迹,实时地来预测下一步可能会加载的内容,作为一个旁路辅助。这里目前并没有想到一个很好的办法。

缓存

鉴于目前前端代码普遍存在于CDN的情况,按需加载带来的另外一个非常有优势的地方在于CDN缓存,假设我们有一份jQuery,有两个文件分别依赖了jQuery,情况如下:

a.js

1
2
3
var jQuery = function(){ /*jquery代码*/ }

jQuery.query( 'xxx' );

b.js

1
2
3
var jQuery = function(){ /*jquery代码*/ }

jQuery.query( 'xxx' );

用户在打开A页面和B页面的时候,分别需要下载a.jsb.js,这时候重复下载了两份jQuery,即使他们是一样的,同时在用户的磁盘,也缓存了两份jQuery

按需加载后的CDN缓存

那么如果我们用按需加载的方式呢?

http://cdn.com/jquery.js

1
var jQuery = function(){ /*jquery代码*/ }

a.js

1
2
3
var jQuery = import( 'http://cdn.com/jquery.js' );

jQuery.query( 'xxx' );

b.js

1
2
3
var jQuery = import( 'http://cdn.com/jquery.js' );

jQuery.query( 'xxx' );

jQuery.js只会被下载一次,所有页面如果用的都是一个cdn地址,那么只要用户打开过任何一个页面,之后的页面即使用户从未打开过,jQuery也已经被缓存,这带来的是一个1+1>2的效果。

理想中的按需加载

最后说一说我理想中的按需加载,看下图:

  1. 逻辑层和展示层完全剥离。
  2. 用户操作一个页面的顺序,一定是:
    1. 眼睛看到展示层,发现有一个按钮。
    2. 大脑告诉你,这个是一个按钮,是可以点击的。
    3. 用手控制鼠标去点击按钮。
    4. 执行点击按钮之后事情。
  3. 首屏加载的时候,我们可以只加载展示层,逻辑层代码一定是在用户点击按钮之后才会执行。
  4. 首屏加载完成,利用用户看页面,并点击按钮的时间,预加载逻辑层代码,实现最快速的首屏展示。

什么是MTU?为什么MTU值普遍都是1500?

大学那会我玩魔兽世界,我的职业是法师,然后经常有朋友找我我带小号,带小号的方式是冲到血色副本里面把所有怪拉到一起,然后一起用AOE技能瞬间杀掉,在学校玩的时候没什么问题,但是放假在家的时候,我发现每次我拉好怪,放技能AOE的那个瞬间,很大概率会掉线,也不是网速问题,当时很多人也遇到同样的问题,看到个帖子说,把自己的MTU改成1480就行了,当时也不知道啥是MTU,就改了,发现还真的可以,就愉快地打游戏去了,多年以后我才知道MTU的重要性。

什么是MTU

Maximum Transmission Unit,缩写MTU,中文名是:最大传输单元。

这是哪一层网络的概念?

从下面这个表格中可以看到,在7层网络协议中,MTU是数据链路层的概念。MTU限制的是数据链路层的上层协议的payload的大小,例如IP,ICMP等。

OSI中的层 功能 TCP/IP协议族
应用层 文件传输,电子邮件,文件服务,虚拟终端 TFTP,HTTP,SNMP,FTP,SMTP,DNS,Telnet
表示层 数据格式化,代码转换,数据加密 没有协议
会话层 解除或建立与别的接点的联系 没有协议
传输层 提供端对端的接口 TCP,UDP
网络层 为数据包选择路由 IP,ICMP,RIP,OSPF,BGP,IGMP
数据链路层 传输有地址的帧以及错误检测功能 SLIP,CSLIP,PPP,ARP,RARP,MTU
物理层 以二进制数据形式在物理媒体上传输数据 ISO2110,IEEE802,IEEE802.2

MTU有什么用?

举一个最简单的场景,你在家用自己的笔记本上网,用的是路由器,路由器连接电信网络,然后访问了www.baidu.com,从你的笔记本出发的一个以太网数据帧总共经过了以下路径:

1
笔记本 -> 路由器 -> 电信机房 -> 服务器

其中,每个节点都有一个MTU值,如下:

1
2
1500     1500                 1500
笔记本 -> 路由器 -> 电信机房 -> 服务器

假设现在我把笔记本的MTU最大值设置成了1700,然后发送了一个超大的ip数据包(2000),这时候在以外网传输的时候会被拆成2个包,一个1700,一个300,然后加上头信息进行传输。

1
2
1700     1500                1500
笔记本 -> 路由器 -> 电信机房 -> 服务器

路由器接收到了一个1700的帧,发现大于自己设置的最大值:1500,如果IP包DF标志位为1,也就是不允许分包,那么路由器直接就把这个包丢弃了,根本就不会到达电信机房,也就到不了服务器了,所以,到这里我们就会发现,MTU其实就是在每一个节点的管控值,只要是大于这个值的数据帧,要么选择分片,要么直接丢弃。

为什么是1500?

其实一个标准的以太网数据帧大小是:1518,头信息有14字节,尾部校验和FCS占了4字节,所以真正留给上层协议传输数据的大小就是:1518 - 14 - 4 = 1500,那么,1518这个值又是从哪里来的呢?

假设取一个更大的值

假设MTU值和IP数据包大小一致,一个IP数据包的大小是:65535,那么加上以太网帧头和为,一个以太网帧的大小就是:65535 + 14 + 4 = 65553,看起来似乎很完美,发送方也不需要拆包,接收方也不需要重组。

那么假设我们现在的带宽是:100Mbps,因为以太网帧是传输中的最小可识别单元,再往下就是0101所对应的光信号了,所以我们的一条带宽同时只能发送一个以太网帧。如果同时发送多个,那么对端就无法重组成一个以太网帧了,在100Mbps的带宽中(假设中间没有损耗),我们计算一下发送这一帧需要的时间:

1
( 65553 * 8 ) / ( 100 * 1024 * 1024 ) ≈ 0.005(s)

在100M网络下传输一帧就需要5ms,也就是说这5ms其他进程发送不了任何数据。如果是早先的电话拨号,网速只有2M的情况下:

1
( 65553 * 8 ) / ( 2 * 1024 * 1024 ) ≈ 0.100(s)

100ms,这简直是噩梦。其实这就像红绿灯,时间要设置合理,交替通行,不然同一个方向如果一直是绿灯,那么另一个方向就要堵成翔了。

既然大了不行,那设置小一点可以么?

假设MTU值设置为100,那么单个帧传输的时间,在2Mbps带宽下需要:

1
( 100 * 8 ) / ( 2 * 1024 * 1024 ) * 1000 ≈ 5(ms)

时间上已经能接受了,问题在于,不管MTU设置为多少,以太网头帧尾大小是固定的,都是14 + 4,所以在MTU为100的时候,一个以太网帧的传输效率为:

1
( 100 - 14 - 4 ) / 100 = 82%

写成公式就是:( T - 14 - 4 ) / T,当T趋于无穷大的时候,效率接近100%,也就是MTU的值越大,传输效率最高,但是基于上一点传输时间的问题,来个折中的选择吧,既然头加尾是18,那就凑个整来个1500,总大小就是1518,传输效率:

1
1500 / 1518 =  98.8%

100Mbps传输时间:

1
( 1518 * 8 ) / ( 100 * 1024 * 1024 ) * 1000 = 0.11(ms)

2Mbps传输时间:

1
( 1518 * 8 ) / ( 2 * 1024 * 1024 ) * 1000 = 5.79(ms)

总体上时间都还能接受

最小值被限制在64

为什么是64呢?

这个其实和以太网帧在半双工下的碰撞有关,感兴趣的同学可以自行去搜索。

在我玩游戏的时候,为什么把MTU改成1480就不卡了?

路由器默认值大多都是1500,理论上是没有问题的,那为什么我玩游戏的时候改成1480才能流畅呢?原因在于当时我使用的是ADSL上网的方式,ADSL使用的PPPoE协议。

PPPoE

PPPoE协议介于以太网和IP之间,协议分为两部分,PPP( Point to Point Protocol )和oE( over Ethernet ),也就是以太网上的PPP协议,而PPPoE协议头信息为:

1
| VER(4bit) | TYPE(4bit) | CODE(8bit) | SESSION-ID(16bit) | LENGTH(16bit) |

这里总共是48位,也就是6个字节,那么另外2个字节是什么呢?答案是PPP协议的ID号,占用两个字节,所以在PPPoE环境下,最佳MTU值应该是:1500 - 4 - 2 = 1492。

我的上网方式

当时我的上网路径如下:

1
PC -> 路由器 -> 电信

我在路由器进行拨号,然后PC连接路由器进行上网。

最根本原因

问题就出在路由器拨号,如果是PC拨号,那么PC会进行PPPoE的封装,会按照MTU:1492来进行以太网帧的封装,即使通过路由器,路由器这时候也只是转发而已,不会进行拆包。

而当用路由器拨号时,PC并不知道路由器的通信方式,会以网卡的设置,默认1500的MTU来进行以太网帧的封装,到达路由器时,由于路由器需要进行PPPoE协议的封装,加上8字节的头信息,这样一来,就必须进行拆包,路由器把这一帧的内容拆成两帧发送,一帧是1492,一帧是8,然后分别加上PPPoE的头进行发送。

平时玩游戏不卡,是因为数据量路由器还处理得过来,而当进行群怪AOE的时候,由于短时间数据量过大,路由器处理不过来,就会发生丢包卡顿的情况,也就掉线了。

帖子里面提到的1480,猜测可能是尽量设小一点,避免二次拨号带来的又一次PPPoE的封装,因为时间久远,没办法回到当时的场景再去抓包了。

结论

1518这个值是考虑到传输效率以及传输时间而折中选择的一个值,并且由于目前网络链路中的节点太多,其中某个节点的MTU值如果和别的节点不一样,就很容易带来拆包重组的问题,甚至会导致无法发送。

前端如何获取主域名(根域名)

背景

最近项目中需要获取url的主域名,比如www.baidu.com那么就需要获取baidu.com,看似简单,.号分隔,取到最后两位就行,但是坑爹的是有xxx.com.cn这类域名,还有很多日本的域名,类似toei.aichi.jp等,这些都无法通过这种简单的取最后两位的方式来获取,看来只能枚举了。

Public Suffix List

这问题肯定是早有人就遇到了,于是各路有识之士已经帮你完整得准备好了一个列表,里面全部都是那些奇葩域名,一些jp域名也是让我长见识了,不知道各位老司机在秋名山飙车的时候有没有见过这些个域名:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
秋田.jp
群馬.jp
香川.jp
高知.jp
鳥取.jp
鹿児島.jp
// jp geographic type names
// http://jprs.jp/doc/rule/saisoku-1.html
*.kawasaki.jp
*.kitakyushu.jp
*.kobe.jp
*.nagoya.jp
*.sapporo.jp
*.sendai.jp
*.yokohama.jp
!city.kawasaki.jp
!city.kitakyushu.jp
!city.kobe.jp
!city.nagoya.jp
!city.sapporo.jp
!city.sendai.jp
!city.yokohama.jp
// 4th level registration
aisai.aichi.jp

感兴趣的朋友可以看看这个github项目:https://github.com/wrangr/psl

这里有各种主域名的列表:https://publicsuffix.org/list/public_suffix_list.dat.

浏览器其实也有内置类似的东西,用来做域名判断,cookie存储之类的事宜。

pls的问题

问题看似好像解决了,已经有现成的脚本去获取,但是仔细一看这脚本竟然有将近200K,而我自己的脚本才10K,既然浏览器已经内置了pls,那浏览器有没有暴露内置接口呢?很遗憾,搜索了一下并没有,而且浏览器那么多,即使chrome暴露了,IE肯定没有,等等,刚刚好像我们说到浏览器用来做域名判断,cookie存储,那我们能不能用这类方式间接地去调用内置pls呢?

最终解决方案

目前想到有两种方式可以间接去调,document.doamindocument.cookie,测试一下就会发现,如果你尝试把当前域名设置为com.cn或者把cookie设置到com.cn上面,浏览器并不会生效,document.domain在第二次设置的时候,firefox会抛错,看来并不是很合适,而且可能多多少少会影响到业务,cookie设置方便,而且清除也方便,上代码:

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
function getMainHost() {
let key = `mh_${Math.random()}`;
let keyR = new RegExp( `(^|;)\\s*${key}=12345` );
let expiredTime = new Date( 0 );
let domain = document.domain;
let domainList = domain.split( '.' );

let urlItems = [];
// 主域名一定会有两部分组成
urlItems.unshift( domainList.pop() );
// 慢慢从后往前测试
while( domainList.length ) {
urlItems.unshift( domainList.pop() );
let mainHost = urlItems.join( '.' );
let cookie = `${key}=${12345};domain=.${mainHost}`;

document.cookie = cookie;

//如果cookie存在,则说明域名合法
if ( keyR.test( document.cookie ) ) {
document.cookie = `${cookie};expires=${expiredTime}`;
return mainHost;
}
}
}

拉了差不多几十个pls里面的域名,跑了一下单元测试,没有问题。

iOS大保健 - 基础信息的收集

本文列出了各种iOS的基础信息的获取方式。

系统信息

  • 设备型号
1
2
3
struct utsname systemInfo;
uname(&systemInfo);
NSString *platform = [NSString stringWithCString:systemInfo.machine encoding:NSASCIIStringEncoding];
  • 操作系统名称
1
2
UIDevice *device = [UIDevice currentDevice];
[device systemName];
  • 操作系统版本
1
2
UIDevice *device = [UIDevice currentDevice];
[device systemVersion];
  • 设备型号,如: iPad,iPod等。
1
2
UIDevice *device = [UIDevice currentDevice];
[device model];
  • 屏幕分辨率
1
2
3
4
5
6
UIScreen *screen     = [UIScreen mainScreen];
CGSize screenSize = [[UIScreen mainScreen] bounds].size;
CGFloat scale = screen.scale;
int screenX = screenSize.width * scale;
int screenY = screenSize.height *scale;
NSString *resolution = [NSString stringWithFormat:@"%d*%d", screenX, screenY];
  • 电池电量
1
2
3
4
5
6
[UIDevice currentDevice].batteryMonitoringEnabled = YES;
[[NSNotificationCenter defaultCenter] addObserverForName:UIDeviceBatteryLevelDidChangeNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notification) {
// Level has changed
NSLog(@"Battery Level Change");
NSLog(@"电池电量:%.2f", [UIDevice currentDevice].batteryLevel);
}];

网络信息

需要引入库:CoreTelephony.framework

  • 网络类型,如:WiFi,4G等。
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
NSString *networktype = nil;
NSArray *subviews = [[[[UIApplication sharedApplication] valueForKey:@"statusBar"] valueForKey:@"foregroundView"]subviews];
NSNumber *dataNetworkItemView = nil;
for (id subview in subviews) {
if([subview isKindOfClass:[NSClassFromString(@"UIStatusBarDataNetworkItemView") class]]) {
dataNetworkItemView = subview;
break;
}
}
switch ([[dataNetworkItemView valueForKey:@"dataNetworkType"]integerValue]) {
case 0:
networktype = @"UNKNOW";
break;
case 1:
networktype = @"2G";
break;
case 2:
networktype = @"3G";
break;
case 3:
networktype = @"4G";
break;
case 4:
networktype = @"LTE";
break;
case 5:
networktype = @"Wi-Fi";
break;
default:
break;
}
return networktype;
  • 蜂窝网络类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CTTelephonyNetworkInfo *telephonyInfo = [[CTTelephonyNetworkInfo alloc] init];
if ( [telephonyInfo respondsToSelector:@selector(currentRadioAccessTechnology)] ) {
id radioAccessType = [telephonyInfo performSelector:@selector(currentRadioAccessTechnology)
withObject:nil];

if ( [radioAccessType isKindOfClass:[NSString class]] ) {
NSString *accessType = (NSString *)radioAccessType;
if ( [accessType hasPrefix:@"CTRadioAccessTechnology"] && accessType.length >= 23 ) {
NSString *accessTypeStr = [accessType substringFromIndex:23];
return accessTypeStr;
}
}
}

return @"UNKNOW";
  • 运营商信息
1
2
3
4
5
6
7
CTTelephonyNetworkInfo *telephonyInfo = [[CTTelephonyNetworkInfo alloc] init];
CTCarrier *carrier = [telephonyInfo subscriberCellularProvider];
NSString *carrierName = [carrier carrierName];
if ( nil != carrierName ) {
return carrierName;
}
return @"UNKNOW";

应用状态

  • APP启动,或者由后台进入前台
1
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationBecomeActive) name:UIApplicationDidBecomeActiveNotification object:nil];
  • APP从后台进入前台
1
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationBecomeActive) name:UIApplicationWillEnterForegroundNotification object:nil];
  • APP进入后台
1
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(applicationEnterBackground) name: UIApplicationDidEnterBackgroundNotification object:nil];
  • APP在前台锁屏
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#import <notify.h>
#define NotificationLock CFSTR("com.apple.springboard.lockcomplete")
#define NotificationChange CFSTR("com.apple.springboard.lockstate")
#define NotificationPwdUI CFSTR("com.apple.springboard.hasBlankedScreen")

static void screenLockStateChanged(CFNotificationCenterRef center,void* observer,CFStringRef name,const void* object,CFDictionaryRef userInfo)
{
NSString* lockstate = (__bridge NSString*)name;
if ([lockstate isEqualToString:(__bridge NSString*)NotificationLock]) {
NSLog(@"locked.");
} else {
NSLog(@"lock state changed.");
}
}

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), NULL, screenLockStateChanged, NotificationLock, NULL, CFNotificationSuspensionBehaviorDeliverImmediately);
CFNotificationCenterAddObserver(CFNotificationCenterGetDarwinNotifyCenter(), NULL, screenLockStateChanged, NotificationChange, NULL, CFNotificationSuspensionBehaviorDeliverImmediately);
return YES;
}
  • APP在后台锁屏,通过循环的方式检测
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static void setScreenStateCb()
{
uint64_t locked;

__block int token = 0;
notify_register_dispatch("com.apple.springboard.lockstate",&token,dispatch_get_main_queue(),^(int t){
});
notify_get_state(token, &locked);
NSLog(@"%d",(int)locked);
}

- (void)applicationDidEnterBackground:(UIApplication *)application
{
while (YES) {
setScreenStateCb();
sleep(5); // 循环5s
}
}
  • 获取当前APP的CPU使用率
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
52
53
54
55
56
57
58
kern_return_t kr;
task_info_data_t tinfo;
mach_msg_type_number_t task_info_count;

task_info_count = TASK_INFO_MAX;
kr = task_info(mach_task_self(), TASK_BASIC_INFO, (task_info_t)tinfo, &task_info_count);
if (kr != KERN_SUCCESS) {
return;
}

task_basic_info_t basic_info;
thread_array_t thread_list;
mach_msg_type_number_t thread_count;

thread_info_data_t thinfo;
mach_msg_type_number_t thread_info_count;

thread_basic_info_t basic_info_th;
uint32_t stat_thread = 0; // Mach threads

basic_info = (task_basic_info_t)tinfo;

// get threads in the task
kr = task_threads(mach_task_self(), &thread_list, &thread_count);
if (kr != KERN_SUCCESS) {
return;
}
if (thread_count > 0)
stat_thread += thread_count;

long tot_sec = 0;
long tot_usec = 0;
float tot_cpu = 0;
int j;

for (j = 0; j < thread_count; j++)
{
thread_info_count = THREAD_INFO_MAX;
kr = thread_info(thread_list[j], THREAD_BASIC_INFO,
(thread_info_t)thinfo, &thread_info_count);
if (kr != KERN_SUCCESS) {
return;
}

basic_info_th = (thread_basic_info_t)thinfo;

if (!(basic_info_th->flags & TH_FLAGS_IDLE)) {
tot_sec = tot_sec + basic_info_th->user_time.seconds + basic_info_th->system_time.seconds;
tot_usec = tot_usec + basic_info_th->system_time.microseconds + basic_info_th->system_time.microseconds;
tot_cpu = tot_cpu + basic_info_th->cpu_usage / (float)TH_USAGE_SCALE * 100.0;
}

} // for each thread

kr = vm_deallocate(mach_task_self(), (vm_offset_t)thread_list, thread_count * sizeof(thread_t));
assert(kr == KERN_SUCCESS);

NSLog(@"CPU Usage: %f \n", tot_cpu);
  • 获取当前APP的内存使用情况
1
2
3
4
5
6
7
8
9
10
task_basic_info_data_t taskInfo;
mach_msg_type_number_t infoCount = TASK_BASIC_INFO_COUNT;
kern_return_t kernReturn = task_info(mach_task_self(),
TASK_BASIC_INFO, (task_info_t)&taskInfo, &infoCount);

if(kernReturn != KERN_SUCCESS) {
return;
}

NSLog(@"Memory Usage: %f", taskInfo.resident_size / 1024.0 / 1024.0);