戴铭的博客 - 星光社

在快手做分享、无用类检查、在广州做 SwiftUI 学习笔记分享、InfoQ二叉树视频

字数统计: 3.1k阅读时长: 11 min
2020/01/05 Share

在快手做分享

前滴滴同事邀请我去快手做分享。下面是分享时的 Slides:
















详细文章介绍:如何对 iOS 启动阶段耗时进行分析 | 星光社 - 戴铭的博客

代码:GitHub - ming1016/MethodTraceAnalyze: 方法耗时分析

无用类检查

如果包里有一堆没用的类,不光会影响用户下载速度,也会影响启动加载速度。检查无用类,一次是无法获得全部无用类的,因为无用的类里用了其他无用的类就算是有用了,所以需要进行递归查找,这样才能够连根拔起。这个过程如果是手动做比较费劲、收益无法一次评估,很难推动。同时还需要在线上灰度运行时检查实际类的使用情况,很多静态层面关联的类使用,实际运行过程中也可能用不到。

思路和关键代码如下。

第一步

使用 MethodTraceAnalyze 里 ParseOC 类的 ocNodes 函数,通过传入 workspace 路径获取所有节点的结构体 OCNode。

1
let allNodes = ParseOC.ocNodes(workspacePath: workSpacePath)

找出类型是方法的结构体,因为类的初始化和使用都是在这些方法中进行的。OCNode 针对不同类型所存储的数据也是不同的,所以我定义一个 OCNodeValueProtocol 协议属性,这样就可以针对不同类型的节点存储不同的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public struct OCNodeDefaultValue: OCNodeValueProtocol {
public var defaultValue: String
init() {
defaultValue = ""
}
}
public struct OCNodeMethod: OCNodeValueProtocol {
public var belongClass: String
public var methodName: String
public var tokenNodes: [OCTokenNode]
}
public struct OCNodeClass: OCNodeValueProtocol {
public var className: String
public var baseClass: String
public var hMethod: [String]
public var mMethod: [String]
public var baseClasses: [String]
}

可以看到对方法类型会存所属类、方法名和方法内所有 token以便进行进一步分析。对类这种类型会记录他的基类、类名、头文件方法列表和实现文件方法列表,还用一个栈记录继承链。

第二步

获取所有类的节点,通过对方法内所有 token 的分析来看使用了哪些类,并记录使用的类。

获取所有类节点的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
// 获取所有类节点
var allClassSet:Set<String> = Set()
for aNode in allNodes {
if aNode.type == .class {
let classValue = aNode.value as! OCNodeClass
allClassSet.insert(classValue.className)
if classValue.baseClass.count > 0 {
baseClasses.insert(classValue.baseClass)
}
}
} // end for aNode in allNodes

记录使用的类关键代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static func parseAMethodUsedClass(node: OCNode, allClass: Set<String>) -> Set<String> {
var usedClassSet:Set<String> = Set()
guard node.type == .method else {
return usedClassSet
}
let methodValue:OCNodeMethod = node.value as! OCNodeMethod
for aNode in methodValue.tokenNodes {
if allClass.contains(aNode.value) {
usedClassSet.insert(aNode.value)
}
}
return usedClassSet
}

第三步

有了所有使用的类和所有的类,就能够获取没用到的类。为了跑一次就能够将所有没用的类找出,所以需要在找到无用类后,将这些类自动去掉再进行下一次查找。我这里写了个递归来干这件事。具体代码如下:

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
var recursiveCount = 0
func recursiveCheckUnUsedClass(unUsed:Set<String>) -> Set<String> {
recursiveCount += 1
print("into recursive!!!!第\(recursiveCount)次")
print("----------------------\n")
for a in unUsed {
print(a)
}
var unUsedClassSet = unUsed
// 缩小范围
for aUnUsed in unUsedClassSet {
if allClassSet.contains(aUnUsed) {
allClassSet.remove(aUnUsed)
}
}
var allUsedClassSet:Set<String> = Set()
for aNode in allNodes {
if aNode.type == .method {
let nodeValue:OCNodeMethod = aNode.value as! OCNodeMethod
// 过滤已判定无用类里的方法
guard !unUsedClassSet.contains(nodeValue.belongClass) else {
continue
}
let usedSet = ParseOCMethodContent.parseAMethodUsedClass(node: aNode, allClass: allClassSet)
if usedSet.count > 0 {
for aSet in usedSet {
allUsedClassSet.insert(aSet)
}
} // end if usedSet.count > 0
} // end if aNode.type == .method
} // end for aNode in allNodes
var hasUnUsed = false
// 找出无用类
for aSet in allClassSet {
if !allUsedClassSet.contains(aSet) {
unUsedClassSet.insert(aSet)
hasUnUsed = true
}
}
if hasUnUsed {
// 如果发现还有无用的类,需要继续递归调用进行分析
return recursiveCheckUnUsedClass(unUsed: unUsedClassSet)
}
return unUsedClassSet
}
// 递归调用
var unUsedClassFromRecursive = recursiveCheckUnUsedClass(unUsed: Set<String>())

通过递归进行多次能够取到最终的结果。

第四步

对于继承和系统的类还需要进行过滤,进一步提高准确性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let unUsedClassSetCopy = unUsedClassFromRecursive
for aSet in unUsedClassSetCopy {
// 过滤系统控件
let filters = ["NS","UI"]
var shouldFilter = false
for filter in filters {
if aSet.hasPrefix(filter) {
shouldFilter = true
}
}
// 过滤基类
if baseClasses.contains(aSet) {
shouldFilter = true
}
// 开始过滤
if shouldFilter {
unUsedClassFromRecursive.remove(aSet)
}
}

清理了通过这种静态扫描出的无用类,还可以通过运行时来判断类是否被初始化了,从而找出无用类。类运行时是否初始化的这个信息是个布尔值,叫 isInitialized,存储在元类 class_rw_t 结构体的 flags 字段里,在 1<<29 位记录。

完整代码见 ParseOCMethodContent 文件:MethodTraceAnalyze/ParseOCMethodContent.swift at master · ming1016/MethodTraceAnalyze · GitHub

在广州做的 SwiftUI 学习笔记分享


下面是笔记内容:





















推荐喵神的 SwiftUI 新书,ObjC 中国 - SwiftUI 与 Combine 编程

这本介绍 Combine 的书也介绍的非常详细:Using Combine

这个网站有大量 SwiftUI 的控件使用范例可以参考:SwiftUI by Example - free quick start tutorials for Swift developers

这个博客每篇都是 SwiftUI 相关的,而且更新非常频繁:Home | Majid’s blog about Swift development

InfoQ二叉树视频

五分钟的视频,在导演构思下需要一天在四个地方进行拍摄,由于前一天晚上庆功宴喝高了,拍摄当天 iPad 笔也忘带了,头还有些懵。导演中午饭都没吃专门回家拿了他的笔给我用。下午地点安排在央美,先访谈再画一张。北京电影学院毕业14年专业绘画经验的导演贾成斌,在我画时边帮我改画边传授了经验,我觉得这些经验会让我更进一步。

下面是记者剡沛在 InfoQ 上发布的采访内容和视频,原文在:“创造,就值得被肯定”,一名程序员的艺术人生丨二叉树视频

他是一名程序员,同时也用自己的业余时间画画。无论是技术分享还是珍藏回忆,他都用画笔记录自己,连接他人。他觉得程序员很酷,无论编程还是画画,都是在创造,这就是最值得肯定的事情。

他在高德负责架构研发工作,也是大家眼中的艺术家,在他身上总能看到那些执念与决心,它们发着光,无时无刻不影响着周围的人。

他就是戴铭,一名酷酷的程序员。

当聊到”连接“这个词的时候,他的眼神异常坚定。

他觉得自己坚持创作,坚持做很多没有门槛的技术分享,很大一部分动力就来自这种渴望,渴望连接自己的过去,也渴望连接他人。

他就是戴铭,一个有点酷,还有点文艺的程序员,在高德地图负责架构研发工作。除了把自己活的很年轻,在他身上总能看到一些发着光的东西。

“是信念吗?”

“是执念。”

“当漫画家,可能连饭都吃不饱。”

故事的开头,多少有些遗憾。

戴铭最早接触画画,是小学之前报过的一个高阶国画班,因为老师在上海,所以他每画完一张都要寄过去并等待回信,当其中一幅画改到第三遍的时候老师回信说:这孩子可能没什么天赋。因为这件事,当时戴铭心里对画画的渴望,几乎降到了冰点,对于画画的兴趣也就此搁置。

直到六年级的一次美术作业,平时酷爱看漫画的戴铭,才再一次下定决心把自己喜欢的角色搬到纸上。

“同学都说画的太像了,那种被再次肯定的开心,很难忘记。”

后来整个初中,戴铭都在课余时间画漫画,也没再报班,一直到初中毕业戴铭跟父亲说不想上学了,“想去画漫画,做一名漫画家”。不难预料,这个想法并没有得到父亲的支持,“当漫画家,可能连饭都吃不饱”。

但从戴铭的话语中,并没有因为父亲这次选择而听到丝毫气馁,似乎心里的种子已经生根。

“兴趣不是说喜欢漫画,就要去从事漫画。当我们从被动的行为中获得成就感时,也会在无形中培养出兴趣,画画如此,编程也是一样。“

“我不想让自己投入了生命的热爱,停滞不前。”

“从那后来,就一直在坚持画画了。”

从临摹简单的漫画,到更复杂的画风、更多元的角色,再到临摹写实人物、影视剧照,期间还专门自学过素描,直到现在的再创作,戴铭除了把自己的爱好和回忆画出来,还把自己的专业内容做成漫画,用更容易传达的方式去做每一次技术分享。

“大概是从四、五年前开始,空闲的时候会花很长的时间画画,平时每天也会挤出一个多小时坚持练手,因为我不希望自己热爱的东西,停滞不前,兴趣不该只是兴趣而已。”

后来戴铭接触了数绘,就开始把很多创作留在屏幕上。

“用 ipad 画,更适合我现在的角色,因为可以随时开始和结束,不受环境和工具的影响,另一方面数字绘图也让他的作品在色彩方面,有了更多的提升空间。”

从容地掏出平板,只要自己想,就能随时还原周遭的一切。这种感觉,就跟戴铭看待程序员时的表达一样,“都是很酷的事情,因为无论程序员的人还是艺术家,他们都在创造新的事物,仅这一点,就值得被肯定。”

“工作不用心的人,生活也不会太精彩”

类似画画这种需要大量时间去“熬”的爱好,坚持总是最难的部分。

“时间永远都是紧缺的,这是肯定。如果工作很忙,就先把时间全部投入到工作上,用最快的速度做好、做完,才能有更多的精力和心情去做其他事情”。

戴铭上一份工作在滴滴,刚入职就希望拓宽自己的能力范围,几乎承担整个部门的研发任务,后来临近发版 Bug 实在解不完,第二天来公司发现都被领导默默解掉了,才意识到一个人的力量始终有限,“一个手指,肯定比不过一个拳头的力量。”

即便如此,戴铭也始终保持着对工作的热血,当时间不够用的时候,他会换个角度去看待问题。

“我觉得工作上面不上心、不拼命的人,生活也不会太精彩。工作是跟每个人的一生切实相关的事情,如果连这个都做不好,又如何能在其他事情上更用心的经营?”

时间看透了,剩下的就是坚持。

当聊到坚持的原动力时,除了用回忆和分享去推动自己,在戴铭身上总散发着一股劲儿。一个兴趣爱好而已,谈信念可能过于悲壮,所以他认为,这股劲儿更像是决心和执念。

有刚入进入滴滴时,想肩扛所有工作的执念;

有为了兴趣上一个台阶,努力获得央美朋友肯定的执念;

也有怕自己的作品破坏了心中的完美角色,重复打磨的执念。

结尾

戴铭是很强大的人,他讲过的一段话令人印象深刻:

“幸福是面对过去,恐惧是面对未知的未来。我也忘记是从哪里看到的这句话,但我自己会这样理解:回忆是让人幸福的,未知是令人恐惧的。但如果我们沉湎在回忆中不敢面对未来,幸福始终是有限的,当我们用决心去面对未来,幸福就会越来越多,恐惧也会越来越少。”

裹着决心这把利剑,酷酷的戴铭就这样用代码和画笔勾勒着自己的一生,而画卷展开的部分就已经足够精彩,余下的,定会更值得期待。

CATALOG
  1. 1. 在快手做分享
  2. 2. 无用类检查
    1. 2.1. 第一步
    2. 2.2. 第二步
    3. 2.3. 第三步
    4. 2.4. 第四步
  3. 3. 在广州做的 SwiftUI 学习笔记分享
  4. 4. InfoQ二叉树视频
    1. 4.1. “当漫画家,可能连饭都吃不饱。”
    2. 4.2. “我不想让自己投入了生命的热爱,停滞不前。”
    3. 4.3. “工作不用心的人,生活也不会太精彩”
    4. 4.4. 结尾