iOS客户端稳定性体系

客户端稳定性导图

客户端稳定性

客户端稳定性重要性毋庸置疑。在高举用户体验第一的移动时代,稳定性就是用户体验的基线。

这里简单的介绍一下在工作中遇到的和想到的一些思路。

稳定性体系的搭建可分为业务上线前中后三个阶段

  1. 代码上线之前
  2. 代码线上运行
  3. 运行崩溃产生

我们这里讨论的稳定性主要是指崩溃(Crash率),不涵盖其他性能指标(滑动帧率,CPU使用率,内存使用情况等)。

关于 Crash Reports Apple官方有详细的文档Understanding and Analyzing Application Crash Reports

  • 这里需要区别对待的是 崩溃日志的收集,不能依赖官方的日志采集(时效性)。像大厂对这方面有要求的都是自己实现崩溃日志采集并维护一个崩溃日志分析系统(符号化,业务分拣,监控报警)

  • 稳定性线上解决方案,就不得不提热修复了。 这个至关重要,对大厂来说线上一个高频率Crash损失是致命的(对应测试覆盖关键用户操作路径)。业界主流选择是JSPatchwax

稳定性防护

上线前

充分测试是上线前环节是必不可少的。尽量覆盖各种用户操作路径和数据场景,枚举正常流和异常流情况。建立完善的CI体系也是可以提升应用质量

Code Review,这个是老生常谈的一个话题。有过实习时上线代码被组内同学review的经历

将一些安全编码经验沉淀成规则,就有了静态扫描工具。如Xcode自带的Analyze,也可以自定义扫描规则严格控制上线代码逻辑质量。在CI持续集成中可以增加这样的卡口限制不安全代码集成上线

重要版本迭代进行灰度机制,发布bate版本。小范围试用,影响范围可控

线上代码运行时

安全方法

Apple Foundation中提供的常用数据结构,部分接口是不安全的如NSMutableArray- (void)addObject:(ObjectType)anObject 文档说明中提及 插入nil会导致程序出现异常进而崩溃

1
2
3
4
5
6
7
8
9
Description	
Inserts a given object at the end of the array.

Parameters
anObject
The object to add to the end of the array’s content. This value must not be nil.

Important
Raises an NSInvalidArgumentException if anObject is nil.

这些不安全的接口,应该禁止调用。在组内推广使用安全的接口(实现扩展对参数进行校验)

安全气囊

基于Objective-C Runtime Programming Guide介绍中

Message Forwarding
Sending a message to an object that does not handle that message is an error. However, before announcing the error, the runtime system gives the receiving object a second chance to handle the message.

应对 unrecognized selector sent to instance/class 安全气囊实现软着陆,避免崩溃

try catch

Objective-C 中实际编程应用try catch场景并不多。但比如NSJSONSerialization序列化时 出现非JSON数据类型,系统还是会抛出异常让开发者处理的。

1
If obj will not produce valid JSON, an exception is thrown. This exception is thrown prior to parsing and represents a programming error, not an internal error. You should check whether the input will produce valid JSON before calling this method by using isValidJSONObject:.

主要还是开发者编码时的安全意识,防御式编程。使用系统API时,尽量多考虑异常情况的出现。

崩溃产生后

由于上线了有逻辑缺陷的代码导致的崩溃,可由热修复方案来实现替换。

但存在这样一种场景:由于错误数据导致的客户端崩溃。虽然回滚了服务端的发布没有新增设备的崩溃,但老设备持有了脏数据缓存导致应用启动崩溃。这个时候热修复方案就显得有点”棘手”。

  1. 当然可以选择热修复去掉缓存逻辑,并不是很好,至少一个版本用户不能体验到缓存带来的优化。
  2. 热修复也存在时机问题,热修复带有网络交互。除非阻塞式拉取最新补丁逻辑,否则无法及时修复启动崩溃

这里就有一个”安全模式”的思路:
在客户端本地产生超过阈值的崩溃时(高频),自启动或提示用户启动 安全模式逻辑。主要就是针对上述场景做的一次补救,注意 逻辑时机一定要早于任何业务处理时机

导图中枚举了常见的几种脏数据数据缓存场景,具体实现还要结合自身业务发展考虑。

UTF8 编码识别

背景

早些年,java应用的开发环境是windows下使用eclipse。eclipse默认工程中文编码是GBK,这就为以后的业务埋下了隐患。

在大型native客户端开发中,模块按照功能/页面划分解耦。要解除相互之间的符号依赖(编译期间)和业务依赖(运行期间),可以使用”路由中心”,借鉴pc url思想来描述native页面。

恰好,我负责的搜索页面,native url中带有中文参数就牵涉这块编码问题了。

问题

由于业务上后端应用一直是GBK编码,所以搭建起来的一些运营系统对外投放的h5页面数据上中文是使用GBK的。这些上游页面使用url跳转会被”路由中心”拦截到我的页面,并且会将url中的params参数一一对应的填充到我的页面属性中,其中的中文参数会使用GBK解码。

随着开发环境的升级差异和前端同学推广UTF8的使用等,业务上上游页面渐渐出现了使用UTF8编码的情况。我们尝试通过约定来避免线上业务使用乱码的问题,比如:

  1. 强制要求上游业务必须使用GBK编码
  2. url中携带_input_charset参数表示是哪种编码方式(_input_charset=gbk/_input_charset=utf8)

但迭代下来,仍然线上会出现乱码问题 (UTF8编码使用GBK解码)。

主要原因是,历史上搭建的运营系统使用GBK编码,产生的上游页面并不会按照约定2来执行。而新系统正在积极的使用UTF8编码不按照约定1。在可预见的未来,UTF8应该会普及开来。

所以,会了应对乱码问题。客户端需要编码识别能力。

技术方案

我们先确定一下要解决的问题:识别链接中的中文参数是GBK编码还是UTF8编码(注意只有这两种编码情况,且不混合夹杂编码一半是GBK,一半是UTF8的情况)。

我们基于识别的理论基础是:

  • UTF-8 是兼容 ASCII 的,所以 0~127 就和 ASCII 完全一致了。
    GBK 的第一字节是高位为 1 的,第 2 字节可能高位为 0 。这种情况一定是 GBK ,因为 UTF-8 对 >127 的编码一定每个字节高位为 1 。
    另外,对于中文,UTF-8 一定编码成 3 字节。(似乎亚洲文字都是,UTF-8 中双字节好象只用于西方字符集)
    所以型如 110* 10** 的,我们一概看成 GBK/GB2312 编码。这就解决了“位”的问题。
    汉字以及汉字标点(包括日文汉字等),在 UTF8 中一定被编码成:1110 10** 10**

  • 连续汉字数量不是 3 的倍数的 GB2312 编码的汉字字符串一定不会被误认为 UTF-8 。用了一些GBK 扩展字,或是插入了一些 ASCII 符号的字符串也几乎不会被认为是 UTF-8 。

  • 一般说来,只要汉字稍微多几个,GBK 串被误认为 UTF-8 的可能性极其低。(只需要默认不使用 UTF-8 中双字节表示的字符)可能性低,这里还有另外一个原因。UTF-8 中汉字编码的第一个字节是 1110** ,这处于汉字的 GB2312 中二级汉字(不常用汉字,区码从 11011000 开始)的编码空间。一般是一些生僻字才会碰上。

所以,我们重点观察汉字被编码之后,是否符合1110 10** 10**这种情况。我们一开始假设是UTF-8编码,一旦发现不符合的话我们就反推认为是GBK编码。

实现上,正好和leetcode上一道题目相同 393. UTF-8 Validation

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
class Solution {
public:
bool validUtf8(vector<int>& data) {
size_t len = data.size();
for( auto i = 0; i<len; i++ ){
if((data[i]|0x7f) == 0x7f) continue;
int cnt = 0;
for(int j=0;j<3;j++){
if((data[i]&mask[j]) == val[j]){
cnt = j+2;
break;
}
}
if(!cnt) return false;
if(i+cnt > len) return false;
for(int j=1;j<cnt;j++){
if((data[i+j]&0xc0) != 0x80) return false;
}
i += (cnt - 1);
}
return true;
}
private:
int mask[3] = {0xe0,0xf0,0xf8}; // 2,3,4
int val[3] = {0xc0,0xe0,0xf0};
};

额外要注意是对于一些标点符号保留字非ASCII码的单字节是跳过识别的。

线上已经跑了两年多了,再也没有乱码问题的反馈了。

引用

  1. 云风Blog 区分一个包含汉字的字符串是 UTF-8还是GBK

Leetcode Attempted Algorithms

先做一个简短的总结吧,leetcode的目前的进度是 332/632。平台因为每周都会举行Contest,所以题目仍然一直在增加。以我目前投入的精力和时间,应该是切不完(无奈笑)。不过每次提交也是乐在其中。

这次把自己Attempted(提交过,没有通过的)题目拖出来,重点审视一下。这些题目都是自己的短板。

序号 题目 难度 通过率
132 Palindrome Partitioning II Hard 24.0%
218 The Skyline Problem Hard 26.8%
220 Contains Duplicate III Medium 19.1%
222 Count Complete Tree Nodes Medium 27.3%
395 Longest Substring with At Least K Repeating Characters Medium 35.5%
421 Maximum XOR of Two Numbers in an Array Medium 46.6%
437 Path Sum III Easy 39.5%
522 Longest Uncommon Subsequence II Medium 30.4%
673 Number of Longest Increasing Subsequence Medium 31.1%

Palindrome Partitioning II

题目大意

给定一个字符串s,分割成各个子串使其都为回文串。请问最小分割次数为多少。

解题思路

我一开始使用 广度优化搜索 这种暴力的枚举方式。

思路是枚举每一个index,切分得到两个子串 若都为回文串则返回当前步数(广度优化保证为最小答案)。若只有一个子串为回文串,则将另一个子串(非回文串)加入队列接着枚举。若两个都不是,则跳过该index
跑测试用例是没有问题,答案都是正确的。可惜超时了。

1
2
25 / 29 test cases passed.
Status: Time Limit Exceeded

后台给出的用例是这个:"fifgbeajcacehiicccfecbfhhgfiiecdcjjffbghdidbhbdbfbfjccgbbdcjheccfbhafehieabbdfeigbiaggchaeghaijfbjhi"。我在main函数里面加了统计时间方法,是15774.1 ms

这个思路最大的问题在于,如果目标串(s)过长,没有合理剪枝的情况下会产生很多冗余的状态压入队列导致超时。

进而思考,有没有存在剪枝使得枚举过的状态是单一可查的,避免无用低效的穷举。

Longest Substring with At Least K Repeating Characters Accepted

题目大意

在给定的只有小写字母的字符串中找出最长子串T,使得T中的每个字符至少出现过k

解题思路

枚举T字符串的起始位置(index)和长度(length)。构造出字符串之后,开始统计字符出现次数并对比k。

做法上几乎纯模拟,最后超时。用例就不贴了,T巨长。

1
2
27 / 28 test cases passed.
Status: Time Limit Exceeded

最后,借鉴了别人的思路,采用二分法缩小范围。具体看实现代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
int longestSubstring(string s, int k) {
if(k == 0) return (int)s.size();
if(s.size() == 0 || s.size() < k) return 0;
int table[26] = {0};
for (char c : s){
++table[c - 'a'];
}
size_t idx = 0;
while(idx < s.size() && table[s[idx] - 'a'] >= k) ++idx;
if(idx == s.size()) return (int)s.size();
int left = longestSubstring(s.substr(0,idx), k);
int right = longestSubstring(s.substr(idx+1), k);
return max(left, right);
}
};

The Skyline Problem

题目大意

给出一组建筑的数据(Li,Ri,Hi)。Li和Ri表示该ith建筑物的左边x轴值和右边x轴值,Hi表示建筑物高度。三个数据可在二维坐标系第一象限中明确出建筑物的矩形区域。

求打印出”天际线”:这些线上的点在建筑群投影面积的水平线段左侧,通过”天际线”可以描绘出建筑群的投影面积。

解题思路

看题目意识到是一道动态区间求最大值的问题,动态在于区间中的高度最大值是会更新的(建筑彼此之间可以重叠覆盖)。

知道了各个重叠区间的最高值,最后才能知道sky line的key points所在。

按照线段树的做法,提交了一次。结果超内存了,Memory Limit Exceeded

我的线段树,区间节点划分太细了,叶子节点是一个整数。而测试集的范围是[0,10000]。爆内存太正常了,后续的优化方向应该是 将叶子节点优化为区间。

Spring Boot

Building an Application with Spring Boot

GETTING STARTED

开发环境

  • 一个主流文本编辑器(text editor)或一个集成开发环境(IDE)
  • JDK 1.8版本以上
  • Gradle 2.3以上或Maven 3.0以上
1
2
3
4
5
6
7
 ~ mvn -v
Apache Maven 3.5.0 (ff8f5e7444045639af65f6095c62210b5713f426; 2017-04-04T03:39:06+08:00)
Maven home: /usr/local/Cellar/maven/3.5.0/libexec
Java version: 1.8.0_144, vendor: Oracle Corporation
Java home: /Library/Java/JavaVirtualMachines/jdk1.8.0_144.jdk/Contents/Home/jre
Default locale: zh_CN, platform encoding: UTF-8
OS name: "mac os x", version: "10.12.5", arch: "x86_64", family: "mac"

Build

简单的一个maven入手教程

Building Java Projects with Maven

简单的一个gradle入手教程

Building JavaProjects with Gradle

build.gradle

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
buildscript{
repositories{
mavenCentral()
}
dependencies{
classpath("org.springframework.boot:spring-boot-gradle-plugin:1.5.6.RELEASE")
}
}

apply plugin: 'java'
apply plugin: 'idea'
apply plugin: 'eclipse'
apply plugin: 'org.springframework.boot'

sourceCompatibility = 1.8
targetCompatibility = 1.8


jar {
baseName = 'gs-spring-boot'
version = '0.1.0'
}

repositories {
mavenCentral()
}


dependencies {
compile("org.springframework.boot:spring-boot-starter-web"){
exclude module: "spring-boot-starter-tomcat"
}
compile("org.springframework.boot:spring-boot-starter-jetty")
compile("org.springframework.boot:spring-boot-starter-actuator")
testCompile("junit:junit")
}

Create a Simple Web Application

这里是一个简单的页面控制器逻辑

src/main/java/hello/HelloController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package hello;

import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.RequestMapping;

@RestController
public class HelloController {

@RequestMapping("/")
public String index() {
return "Greetings from Spring Boot!";
}

}

@RestController 表示这个类基于Spring MVC框架可以响应网络请求。

@RestMapping匹配\访问路径到index()方法。

当一个请求来自客户端或者是命令行(curl)时,这个方法返回纯文本。
这是因为@RestController联合了@Controller@ResponseBody两个注解在网络请求中响应了数据而不是视图

Create an Application Class

src/main/java/hello/Application.java

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
package hello;

import java.util.Arrays;

import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}

@Bean
public CommandLineRunner commandLineRunner(ApplicationContext ctx) {
return args -> {
System.out.println("Let's inspect the beans provided by Spring Boot:");
String[] beanNames = ctx.getBeanDefinitionNames();
Arrays.sort(beanNames);
for (String beanName : beanNames) {
System.out.println(beanName);
}

};
}

}

main()入口主函数,调用了Spring BootSpringApplication.run()唤起应用程序。

CommandLineRunner方法被标记为@Bean注解,会运行在start up阶段。他打印出了所有Spring Boot框架所有自动注册的服务组件。

运行应用

  1. Gradle

    gs-spring-boot-0.1.0.jarbuild.gradle中的jar blockbaseName+version

    1
    ./gradlew build && java -jar build/libs/gs-spring-boot-0.1.0.jar
  2. Maven

    1
    mvn package && java -jar target/gs-spring-boot-0.1.0.jar

查看对应jar内容

1
jar tvf gs-spring-boot-0.1.0.jar
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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
     0 Tue Aug 29 10:44:00 CST 2017 META-INF/
222 Tue Aug 29 10:44:00 CST 2017 META-INF/MANIFEST.MF
0 Tue Aug 29 10:44:00 CST 2017 BOOT-INF/
0 Tue Aug 29 10:44:00 CST 2017 BOOT-INF/classes/
0 Mon Aug 28 17:36:04 CST 2017 BOOT-INF/classes/hello/
2208 Mon Aug 28 17:36:04 CST 2017 BOOT-INF/classes/hello/Application.class
749 Mon Aug 28 17:36:04 CST 2017 BOOT-INF/classes/hello/HelloController.class
0 Tue Aug 29 10:44:00 CST 2017 BOOT-INF/lib/
2349 Thu Jul 27 07:24:58 CST 2017 BOOT-INF/lib/spring-boot-starter-web-1.5.6.RELEASE.jar
2529 Thu Jul 27 07:25:02 CST 2017 BOOT-INF/lib/spring-boot-starter-jetty-1.5.6.RELEASE.jar
2302 Thu Jul 27 07:25:06 CST 2017 BOOT-INF/lib/spring-boot-starter-actuator-1.5.6.RELEASE.jar
2290 Thu Jul 27 07:24:42 CST 2017 BOOT-INF/lib/spring-boot-starter-1.5.6.RELEASE.jar
725755 Wed Mar 15 13:14:58 CST 2017 BOOT-INF/lib/hibernate-validator-5.3.5.Final.jar
1242477 Mon Jun 12 00:53:20 CST 2017 BOOT-INF/lib/jackson-databind-2.8.9.jar
822491 Thu Jul 20 11:39:20 CST 2017 BOOT-INF/lib/spring-web-4.3.10.RELEASE.jar
915665 Thu Jul 20 11:40:18 CST 2017 BOOT-INF/lib/spring-webmvc-4.3.10.RELEASE.jar
86901 Wed May 31 16:32:34 CST 2017 BOOT-INF/lib/jetty-servlets-9.4.6.v20170531.jar
121362 Wed May 31 16:28:28 CST 2017 BOOT-INF/lib/jetty-webapp-9.4.6.v20170531.jar
34891 Wed May 31 16:38:10 CST 2017 BOOT-INF/lib/websocket-server-9.4.6.v20170531.jar
36848 Wed May 31 16:38:58 CST 2017 BOOT-INF/lib/javax-websocket-server-impl-9.4.6.v20170531.jar
241490 Thu Mar 31 16:58:14 CST 2016 BOOT-INF/lib/apache-el-8.0.33.jar
556647 Thu Jul 27 07:23:38 CST 2017 BOOT-INF/lib/spring-boot-actuator-1.5.6.RELEASE.jar
674636 Thu Jul 27 07:12:50 CST 2017 BOOT-INF/lib/spring-boot-1.5.6.RELEASE.jar
1069471 Thu Jul 27 07:19:22 CST 2017 BOOT-INF/lib/spring-boot-autoconfigure-1.5.6.RELEASE.jar
2312 Thu Jul 27 07:24:42 CST 2017 BOOT-INF/lib/spring-boot-starter-logging-1.5.6.RELEASE.jar
1122794 Thu Jul 20 11:37:08 CST 2017 BOOT-INF/lib/spring-core-4.3.10.RELEASE.jar
273599 Fri Feb 19 13:13:32 CST 2016 BOOT-INF/lib/snakeyaml-1.17.jar
63777 Wed Apr 10 15:02:44 CST 2013 BOOT-INF/lib/validation-api-1.1.0.Final.jar
66023 Wed Mar 15 13:22:08 CST 2017 BOOT-INF/lib/jboss-logging-3.3.1.Final.jar
64982 Tue Sep 27 22:24:16 CST 2016 BOOT-INF/lib/classmate-1.3.3.jar
55784 Sun Jul 03 22:20:36 CST 2016 BOOT-INF/lib/jackson-annotations-2.8.0.jar
282633 Sun Jun 11 17:43:12 CST 2017 BOOT-INF/lib/jackson-core-2.8.9.jar
380667 Thu Jul 20 11:37:16 CST 2017 BOOT-INF/lib/spring-aop-4.3.10.RELEASE.jar
763052 Thu Jul 20 11:37:14 CST 2017 BOOT-INF/lib/spring-beans-4.3.10.RELEASE.jar
1140861 Thu Jul 20 11:37:48 CST 2017 BOOT-INF/lib/spring-context-4.3.10.RELEASE.jar
263371 Thu Jul 20 11:37:28 CST 2017 BOOT-INF/lib/spring-expression-4.3.10.RELEASE.jar
16702 Wed May 31 16:32:14 CST 2017 BOOT-INF/lib/jetty-continuation-9.4.6.v20170531.jar
163922 Wed May 31 16:25:14 CST 2017 BOOT-INF/lib/jetty-http-9.4.6.v20170531.jar
457682 Wed May 31 16:23:48 CST 2017 BOOT-INF/lib/jetty-util-9.4.6.v20170531.jar
128844 Wed May 31 16:24:50 CST 2017 BOOT-INF/lib/jetty-io-9.4.6.v20170531.jar
50392 Wed May 31 16:25:38 CST 2017 BOOT-INF/lib/jetty-xml-9.4.6.v20170531.jar
110502 Wed May 31 16:27:48 CST 2017 BOOT-INF/lib/jetty-servlet-9.4.6.v20170531.jar
199656 Wed May 31 16:37:00 CST 2017 BOOT-INF/lib/websocket-common-9.4.6.v20170531.jar
35362 Wed May 31 16:37:26 CST 2017 BOOT-INF/lib/websocket-client-9.4.6.v20170531.jar
21303 Wed May 31 16:37:50 CST 2017 BOOT-INF/lib/websocket-servlet-9.4.6.v20170531.jar
78056 Wed May 31 16:30:06 CST 2017 BOOT-INF/lib/jetty-annotations-9.4.6.v20170531.jar
160705 Wed May 31 16:38:36 CST 2017 BOOT-INF/lib/javax-websocket-client-impl-9.4.6.v20170531.jar
36611 Fri May 10 13:09:16 CST 2013 BOOT-INF/lib/javax.websocket-api-1.0.jar
309130 Wed Mar 01 20:40:20 CST 2017 BOOT-INF/lib/logback-classic-1.1.11.jar
16515 Thu Mar 16 17:37:30 CST 2017 BOOT-INF/lib/jcl-over-slf4j-1.7.25.jar
4596 Thu Mar 16 17:37:48 CST 2017 BOOT-INF/lib/jul-to-slf4j-1.7.25.jar
23645 Thu Mar 16 17:37:40 CST 2017 BOOT-INF/lib/log4j-over-slf4j-1.7.25.jar
93085 Wed May 31 16:27:16 CST 2017 BOOT-INF/lib/jetty-security-9.4.6.v20170531.jar
41996 Wed May 31 16:36:36 CST 2017 BOOT-INF/lib/websocket-api-9.4.6.v20170531.jar
268784 Wed May 31 16:33:08 CST 2017 BOOT-INF/lib/jetty-client-9.4.6.v20170531.jar
95806 Thu Apr 25 16:52:26 CST 2013 BOOT-INF/lib/javax.servlet-api-3.1.0.jar
55037 Wed May 31 16:29:38 CST 2017 BOOT-INF/lib/jetty-plus-9.4.6.v20170531.jar
26366 Fri Apr 26 19:47:18 CST 2013 BOOT-INF/lib/javax.annotation-api-1.2.jar
53468 Sat Mar 05 14:37:34 CST 2016 BOOT-INF/lib/asm-5.1.jar
47195 Sat Mar 05 14:37:36 CST 2016 BOOT-INF/lib/asm-commons-5.1.jar
475477 Wed Mar 01 20:39:16 CST 2017 BOOT-INF/lib/logback-core-1.1.11.jar
41203 Thu Mar 16 17:36:32 CST 2017 BOOT-INF/lib/slf4j-api-1.7.25.jar
579819 Wed May 31 16:26:32 CST 2017 BOOT-INF/lib/jetty-server-9.4.6.v20170531.jar
29130 Sat Mar 05 14:37:38 CST 2016 BOOT-INF/lib/asm-tree-5.1.jar
0 Tue Aug 29 10:44:00 CST 2017 org/
0 Tue Aug 29 10:44:00 CST 2017 org/springframework/
0 Tue Aug 29 10:44:00 CST 2017 org/springframework/boot/
0 Tue Aug 29 10:44:00 CST 2017 org/springframework/boot/loader/
2415 Thu Jul 27 07:07:34 CST 2017 org/springframework/boot/loader/LaunchedURLClassLoader$1.class
1454 Thu Jul 27 07:07:34 CST 2017 org/springframework/boot/loader/PropertiesLauncher$ArchiveEntryFilter.class
1912 Thu Jul 27 07:07:34 CST 2017 org/springframework/boot/loader/PropertiesLauncher$PrefixMatchingArchiveFilter.class
4599 Thu Jul 27 07:07:34 CST 2017 org/springframework/boot/loader/Launcher.class
1165 Thu Jul 27 07:07:34 CST 2017 org/springframework/boot/loader/ExecutableArchiveLauncher$1.class
0 Tue Aug 29 10:44:00 CST 2017 org/springframework/boot/loader/jar/
2002 Thu Jul 27 07:07:34 CST 2017 org/springframework/boot/loader/jar/JarFile$1.class
10016 Thu Jul 27 07:07:34 CST 2017 org/springframework/boot/loader/jar/Handler.class
3350 Thu Jul 27 07:07:34 CST 2017 org/springframework/boot/loader/jar/JarEntry.class
1427 Thu Jul 27 07:07:34 CST 2017 org/springframework/boot/loader/jar/JarFile$3.class
3104 Thu Jul 27 07:07:34 CST 2017 org/springframework/boot/loader/jar/CentralDirectoryEndRecord.class
430 Thu Jul 27 07:07:34 CST 2017 org/springframework/boot/loader/jar/CentralDirectoryVisitor.class
1300 Thu Jul 27 07:07:34 CST 2017 org/springframework/boot/loader/jar/JarFile$JarFileType.class
10924 Thu Jul 27 07:07:34 CST 2017 org/springframework/boot/loader/jar/JarFileEntries.class
12762 Thu Jul 27 07:07:34 CST 2017 org/springframework/boot/loader/jar/JarFile.class
1540 Thu Jul 27 07:07:34 CST 2017 org/springframework/boot/loader/jar/JarFileEntries$1.class
672 Thu Jul 27 07:07:34 CST 2017 org/springframework/boot/loader/jar/JarURLConnection$1.class
1199 Thu Jul 27 07:07:34 CST 2017 org/springframework/boot/loader/jar/JarFile$2.class
262 Thu Jul 27 07:07:34 CST 2017 org/springframework/boot/loader/jar/JarEntryFilter.class
4457 Thu Jul 27 07:07:34 CST 2017 org/springframework/boot/loader/jar/AsciiBytes.class
4602 Thu Jul 27 07:07:34 CST 2017 org/springframework/boot/loader/jar/CentralDirectoryParser.class
2169 Thu Jul 27 07:07:34 CST 2017 org/springframework/boot/loader/jar/Bytes.class
1629 Thu Jul 27 07:07:34 CST 2017 org/springframework/boot/loader/jar/ZipInflaterInputStream.class
1967 Thu Jul 27 07:07:34 CST 2017 org/springframework/boot/loader/jar/JarFileEntries$EntryIterator.class
306 Thu Jul 27 07:07:34 CST 2017 org/springframework/boot/loader/jar/FileHeader.class
3641 Thu Jul 27 07:07:34 CST 2017 org/springframework/boot/loader/jar/JarURLConnection$JarEntryName.class
9303 Thu Jul 27 07:07:34 CST 2017 org/springframework/boot/loader/jar/JarURLConnection.class
5449 Thu Jul 27 07:07:34 CST 2017 org/springframework/boot/loader/jar/CentralDirectoryFileHeader.class
0 Tue Aug 29 10:44:00 CST 2017 org/springframework/boot/loader/data/
1531 Thu Jul 27 07:07:34 CST 2017 org/springframework/boot/loader/data/ByteArrayRandomAccessData.class
3549 Thu Jul 27 07:07:34 CST 2017 org/springframework/boot/loader/data/RandomAccessDataFile$DataInputStream.class
1862 Thu Jul 27 07:07:34 CST 2017 org/springframework/boot/loader/data/RandomAccessDataFile$FilePool.class
1341 Thu Jul 27 07:07:34 CST 2017 org/springframework/boot/loader/data/RandomAccessData$ResourceAccess.class
3319 Thu Jul 27 07:07:34 CST 2017 org/springframework/boot/loader/data/RandomAccessDataFile.class
551 Thu Jul 27 07:07:34 CST 2017 org/springframework/boot/loader/data/RandomAccessData.class
4698 Thu Jul 27 07:07:34 CST 2017 org/springframework/boot/loader/LaunchedURLClassLoader.class
1533 Thu Jul 27 07:07:34 CST 2017 org/springframework/boot/loader/JarLauncher.class
1468 Thu Jul 27 07:07:34 CST 2017 org/springframework/boot/loader/MainMethodRunner.class
1425 Thu Jul 27 07:07:34 CST 2017 org/springframework/boot/loader/PropertiesLauncher$1.class
3128 Thu Jul 27 07:07:34 CST 2017 org/springframework/boot/loader/ExecutableArchiveLauncher.class
1669 Thu Jul 27 07:07:34 CST 2017 org/springframework/boot/loader/WarLauncher.class
0 Tue Aug 29 10:44:00 CST 2017 org/springframework/boot/loader/archive/
1749 Thu Jul 27 07:07:34 CST 2017 org/springframework/boot/loader/archive/JarFileArchive$EntryIterator.class
3792 Thu Jul 27 07:07:34 CST 2017 org/springframework/boot/loader/archive/ExplodedArchive$FileEntryIterator.class
1068 Thu Jul 27 07:07:34 CST 2017 org/springframework/boot/loader/archive/ExplodedArchive$FileEntry.class
1051 Thu Jul 27 07:07:34 CST 2017 org/springframework/boot/loader/archive/JarFileArchive$JarFileEntry.class
302 Thu Jul 27 07:07:34 CST 2017 org/springframework/boot/loader/archive/Archive$Entry.class
7189 Thu Jul 27 07:07:34 CST 2017 org/springframework/boot/loader/archive/JarFileArchive.class
4974 Thu Jul 27 07:07:34 CST 2017 org/springframework/boot/loader/archive/ExplodedArchive.class
906 Thu Jul 27 07:07:34 CST 2017 org/springframework/boot/loader/archive/Archive.class
1438 Thu Jul 27 07:07:34 CST 2017 org/springframework/boot/loader/archive/ExplodedArchive$FileEntryIterator$EntryComparator.class
399 Thu Jul 27 07:07:34 CST 2017 org/springframework/boot/loader/archive/Archive$EntryFilter.class
273 Thu Jul 27 07:07:34 CST 2017 org/springframework/boot/loader/archive/ExplodedArchive$1.class
18041 Thu Jul 27 07:07:34 CST 2017 org/springframework/boot/loader/PropertiesLauncher.class
0 Tue Aug 29 10:44:00 CST 2017 org/springframework/boot/loader/util/
4887 Thu Jul 27 07:07:34 CST 2017 org/springframework/boot/loader/util/SystemPropertyUtils.class

附录

Gradle vs Maven: Feature Comparison Chart

为什么iOS更新UI操作必须在主线程

为什么iOS更新UI操作必须在主线程

开发iOS应用程序的同学都知道UI操作如果在非主线程进行的话是会Crash的。

但是本质原因可能很少人真正明白。

如果我来回答的话,我的答案是:
UI操作涉及到渲染访问各种View对象的属性,如果是异步操作会有读写问题。加锁呢,性能损耗大(视图层次深,属性多)。所以主线程操作UI,是约定俗成的开发规则。

我们来看看Quora网友关于这个”经典”问题的回答:

Why must the UI always be updated on Main Thread?

来自 Reinder de Vries 的回答:

The first one is that, in Cocoa Touch, the UIApplication gets set up on the main thread

Another reason is graphics rendering: the graphics pipeline of the iPhone is ultimately synchronous.

他的一个理由是:

  1. Cocoa Touch框架中,UIApplication初始化工作是在主线程进行的。而界面上所有的视图都是在UIApplication 实例的叶子节点(内存管理角度),所以所有的手势交互操作都是在主线程上才能响应
  2. 图形渲染在iPhone设备本质上是同步的。图形渲染计算最终要显示的像素值,以每秒60帧的频率刷新到屏幕上,绘制到屏幕的过程实际上就是通过LED display点亮各个像素。这个过程需要一次将所有将要实现的像素刷新到屏幕上(同时的)。如果要异步化的话,对应的你无法确定这个处理过程是否真正的全部完成。

他最后指出,在主线程操作UI。能帮助你避免不少的麻烦和减少产生产品缺陷。当然,有部分UI操作是可异步化的,只要最后的更新操作是在主线程

  • 叠加滤镜效果在视图上
  • 创建动态图形,比如动画

最后推荐了WWDC上关于UI渲染的录像 Building Concurrent User Interfaces on iOS

相关阅读

Core Animation Profile Symbols Not Found

我们在迭代完一个版本之后,需要使用 Instruments 中的一些 Profile 来获取应用性能数据。

测试过程中,尽可能的还原用户真实的使用场景。 比如 应用的 target scheme应该为release,和渠道发布的保持一致。

Core Animation

我们使用Core Animation可以方便的看到 页面滚动时的帧率和cpu计算资源开销还能对应到工程具体方法代码。

但是,有时候我们使用这个工具会出现看不到正常的函数符号的情况。如:

这里的地址对应的方法都没有被符号化,我们找不到那些有问题的代码。

How to fix

在使用Core Animation Profile之前,你应该确认一下 Symbols -> Search paths for dSYMs这个路径下的dSYMs文件是否存在且是对应的。

开发iOS的同学,应该对dSYMs文件并不陌生。每次上传发布应用的时候,都需要archvie归档生成这个dSYMs文件。方便线上发生Crash的时候,可以对日志进行符号处理:可以看到对应崩溃的执行堆栈。

如果,没有找到 dSYMs文件的话,还得再确认工程构建配置中是否允许生成dSYMs文件。 在 Target - Build Setting->Debug Information Format 中将你现在对应的Target scheme设置为 DWARF with dSYM File

如此,这般配置之后,就可以看到正常的符号化之后的代码了。

另外,一些常用的设置项入口被挪到这里了:

iClean 一款Mac OS 软件的诞生

我工作用的Mac Pro是256GB ssd硬盘。开发iOS久了,经常提示磁盘空间不够,其中最大原因是来自Xcode®

可以打开 ~/Library/Developer/Xcode/DerivedData 看看,这里存放了Xcode编译构建过的所有项目工程数据。

这如 watchdog 磁盘清理软件描述提及的那样 automatically cleans up stale cache files。将陈腐失效的缓存数据及时清理掉,你的ssd硬盘可用空间就多起来了。

作为一个要强的iOS开发工程师,自己能做的东西,为什么要用别人的?

所以,我确定做一款自己的磁盘清理软件

watchdog for Xcode

iClean

令人费解的AppKit

脱离nib 和 storyboard 开发方式

nib和storyboard 千好万好。但在多人开发模式下,修改同一份storyborad,产生的冲突简直是灾难。

干掉项目中info.plist 关于Main.storyboard的设置。在main.m中手动绑定AppDelegate

1
2
3
4
5
6
7
#import <Cocoa/Cocoa.h>
#import "AppDelegate.h"
int main(int argc, const char * argv[]) {
AppDelegate *app = [[AppDelegate alloc] init];
[NSApplication sharedApplication].delegate = app;
return NSApplicationMain(argc, argv);
}

按照iOS的开发经验,Window下要放置一个ViewController。需要注意的是重写loadview方法不要调用super

1
Instantiates a view from a nib file and sets the value of the view property.

super会去加载同名的nib文件。正确的纯编码方式是不调用父类方法,再自己设置一个NSView即可。

1
2
3
4
5
- (void)loadView{
self.view = [[NSView alloc] initWithFrame:NSMakeRect(0, 0, iCleanMainWindowWidth, iCleanMainWindowHeight)];
self.view.wantsLayer = YES;
self.view.layer.backgroundColor = [NSColor brownColor].CGColor;
}

NSTableView

编码使用NSTableView的话,要配置使用NSScorllViewNSTableColumn。 具体实践是:

1
2
3
4
5
6
7
8
9
10
11
12
13
_scrollView = [[NSScrollView alloc] initWithFrame:NSMakeRect(iCleanMainWindowWidth - 400, 0, 400, iCleanMainWindowHeight)];
_scrollView.hasVerticalScroller = _scrollView.hasVerticalRuler = YES;
_scrollView.hasHorizontalScroller = _scrollView.hasHorizontalRuler = NO;

_tableView = [[NSTableView alloc] initWithFrame:_scrollView.bounds];
_tableView.delegate = self;
_tableView.dataSource = self;
_tableView.headerView = nil;
_scrollView.contentView.documentView = _tableView;

NSTableColumn *col = [[NSTableColumn alloc] initWithIdentifier:@"col1"];
col.minWidth = 400;
[_tableView addTableColumn:col];

其中NSTableDataSourceNSTableViewDelegate 虽然都是optional的办法。但仍需要实现下面的方法

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
#pragma mark - NSTableViewDelegate
- (NSView *)tableView:(NSTableView *)tableView viewForTableColumn:(NSTableColumn *)tableColumn row:(NSInteger)row{
NSTextField *cell = [tableView makeViewWithIdentifier:@"cellReuseId" owner:self];
if(!cell){
cell = [NSTextField labelWithString:@""];
cell.identifier = @"cellReuseId";
}

NSMutableAttributedString *str = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@ \n%@",self.data[row].path.lastPathComponent,self.data[row].formatSize] attributes:@{NSForegroundColorAttributeName:[NSColor blackColor]}];

cell.placeholderAttributedString = str;
return cell;
}

- (CGFloat)tableView:(NSTableView *)tableView heightOfRow:(NSInteger)row{
return 50.f;
}

- (void)tableViewSelectionDidChange:(NSNotification *)notification{
NSInteger row = [[notification object] selectedRow];
NSLog(@"%ld",row);
}



#pragma mark - NSTableViewDataSource
- (NSInteger)numberOfRowsInTableView:(NSTableView *)tableView{
return self.data.count;
}

注意是 AppKit里面是没有Label控件的,我这是使用的是NSTextField。就是输入框了,借助placeholderString属性能实现文本展示的效果。

目前的产品形态

TinyURL的生成

Encode and Decode TinyURL

这题很有意思。

题目大意是实现一个长链接和短链接的转化。

形式是 http://tinyurl.com/4e9iAk。 只有6位标志包含(0-9),(a-z),(A-Z)

一位有10+26+26 = 62种值。 6位的总数大致是 62^6 - 1。

思路1. 哈希

既然我们想到了hash,就需要搞明白 URL的字符集,就是说URL中有可能会出现哪些字符。发现题目不控制长链接的长度,再加上URL的字符集也超过了62种。只有6位的话,再高明的hash也难免冲突

思路2. 自增ID

62^6次 大约为 2^32 左右。能发放2^32个ID。

自增的问题在于不可逆。

短链无法逆算出长链,存在这个需求的话需要将ID和长链接关联存储起来。

自增在单点服务器上需要加锁,避免多线程访问ID重复。hash没有这个问题,hash算法保证唯一性。加锁QPS高的情况下,会出现排队等待的情况。分布式支持高QPS,不考虑分布式锁同步的话:

有这种的思路:发单双号ID。A服务器只发单号,B服务器只发双号。类比你集群有多少台服务器。

Raw 535. Encode and Decode TinyURL.cpp

WebSocket

背景

现在,很多网站为了实现推送技术,所用的技术都是轮询。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。

而比较新的技术去做轮询的效果是Comet。这种技术虽然可以双向通信,但依然需要反复发出请求。而且在Comet中,普遍采用的长链接,也会消耗服务器资源。

在这种情况下,HTML5定义了WebSocket协议,能更好的节省服务器资源和带宽,并且能够更实时地进行通讯。

握手协议

WebSocket是独立,建立在TCP上的协议。

HTTP的关系是,WebSocket第一次的握手请求是使用HTTP协议:

一个典型的握手请求请求:

1
2
3
4
5
6
7
GET / HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: example.com
Origin: http://example.com
Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
Sec-WebSocket-Version: 13

服务端回应:

1
2
3
4
5
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: fFBooB7FAkLlXgRSz0BT3v4hq5s=
Sec-WebSocket-Location: ws://example.com/

字段说明

  • Connection必须设置Upgrade,表示客户端希望连接升级。
  • Upgrade字段必须设置Websocket,表示希望升级到Websocket协议。
  • Sec-WebSocket-Key是随机的字符串,服务器端会用这些数据来构造出一个SHA-1的信息摘要。把“Sec-WebSocket-Key”加上一个特殊字符串“258EAFA5-E914-47DA-95CA-C5AB0DC85B11”,然后计算SHA-1摘要,之后进行BASE-64编码,将结果做为“Sec-WebSocket-Accept”头的值,返回给客户端。如此操作,可以尽量避免普通HTTP请求被误认为Websocket协议。
  • Sec-WebSocket-Version 表示支持的Websocket版本。RFC6455要求使用的版本是13,之前草案的版本均应当被弃用。
  • Origin字段是可选的,通常用来表示在浏览器中发起此Websocket连接所在的页面,类似于Referer。但是,于Referer不同的是,Origin只包含了协议和主机名称。
  • 其他一些定义在HTTP协议中的字段,如Cookie等,也可以在Websocket中使用。

客户端发送握手请求,要求服务端升级协议。服务端再回复HTTP/1.1 101 Switching Protocols。 自此双通道已搭建,两端使用发送序列化的数据帧传输数据

除此之外,WebSocket也规定了加密数据传输方法,允许使用TLS/SSL对通信进行加密,类似HTTPS。默认情况下,ws协议使用80端口进行普通连接,加密的TLS连接默认使用443端口。

数据帧

出于安全原因考虑。不管WebSocket协议是否基于TLS上,客户端发送的数据帧中必须包含掩码。

  1. 客户端向服务器传输的数据帧必须进行掩码处理:服务器若接收到未经过掩码处理的数据帧,则必须主动关闭连接。
  2. 服务器向客户端传输的数据帧一定不能进行掩码处理。客户端若接收到经过掩码处理的数据帧,则必须主动关闭连接。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
0               1               2               3                <- byte
0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 0 1 2 3 4 5 6 7 <- bit
+-+-+-+-+-------+-+-------------+-----------------------------+
|F|R|R|R| Opcode|M| Payload len | Extended payload length |
|I|S|S|S| (4) |A| (7) | (16/64) |
|N|V|V|V| |S| | (if payload len==126/127) |
| |1|2|3| |K| | |
+-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - +
| Extended payload length continued, if payload len == 127 |
+ - - - - - - - - - - - - - - - +-----------------------------+
| |Masking-key, if MASK set to 1|
+-------------------------------+-----------------------------+
| Masking-key (continued) | Payload Data |
+-------------------------------- - - - - - - - - - - - - - - +
: Payload Data continued ... :
+ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
| Payload Data continued ... |
+-------------------------------------------------------------+

FIN1 bit 指示这个是消息的最后片段。

RSV1,RSV2,RSV3各占1 bit 扩展协议非零值定义,一般为0。

Opcode操作码,占4 bit定义payload Data的意义。

  • %x0 代表一个继续帧
  • %x0 代表一个继续帧
  • %x1 代表一个文本帧
  • %x2 代表一个二进制帧
  • %x3-7 保留用于未来的非控制帧
  • %x8 代表连接关闭
  • %x9 代表ping
  • %xA 代表pong
  • %xB-F 保留用于未来的控制帧

Mask是否存在掩码标志位,占1 bit。若为1,则Masking-key存在掩码。客户端发送的数据帧中必须为1

Payload length负载数据长度。占7 bits,且最高有效位必须为0。所以7 bits只能表示0~127(2^7 - 1),若值为0~125则表示7 bits是有效负载数据长度。若值为126,则表示需要查看后16 bits。若值为127,则表示需要查看后面64 bits

Masking-key 客户端发送数据帧的掩码

payload Data 应用数据

掩码存在的必要性

掩码是由客户端随机选择的32位值。 客户端必须从允许的32位值集合中选择一个新的掩码。 掩码需要是不可预测的;因此,掩码键必须来自一个强大的熵源,且用于给定帧的掩码键必须不容易被服务器/代理预测用于后续帧的掩码键。

掩码键的不可预测性对防止恶意应用的作者选择出现在报文上的字节是必要的。

引用

The WebSocket Protocol
The WebSocket Protocol 中文版