知乎 · 2018 下半年

2018 下半年 · 12 条
想法2018-12-28

这是经济下行带来的问题,要怪也是怪经济不行吧,房价跌只是现象而不是原因。而有房的人里,真正把房子用来住而不卖的也暂时谈不上损失吧。

♥ 0🔁 0💬 0
文章2018-12-26

Kotlin 101

Kotlin 简介

2011 年,作为全球最先进 IDE 开发商之一的 JetBrains 揭露了一项正处于开发中的编程语言 —— Kotlin。它是一门跑在 JVM 上,和 Java 间具有高互操作性的全新语言。JetBrains 通过多年来和各种编程语言打交道的经验,为 Kotlin 整合了多项现代编程语言特性。

2017 年,Google 在 I/O 大会上宣布将 Kotlin 作为 Android 开发的官方支持语言。彼时,Kotlin 的开发者社区开始了爆炸性的增长,而 Netflix、Pinterest、Trello、Kickstarter 等知名公司也早已开始使用 Kotlin。

2018 年,目前 Kotlin 1.3 正式版本已经发布,更多的语言特性得到了支持。而随着官方提供了更多的编译后端,也让 Kotlin 摆脱了 JVM 的束缚,使用 Kotlin 编写的代码可以编译成机器码甚至 JavaScript 而跑在不同的运行环境中。另外,根据 Google 官方的调查,目前已有 40% 的 Android 开发者选择使用 Kotlin 进行编程工作,国内抖音、网易有道词典、大众点评、知乎等大量应用也开始引入 Kotlin。

使用 Kotlin 有什么好处?

既然 Google 已经宣布 Kotlin 成为 Android 开发的官方支持语言,也说明了至少在 Android 开发上使用 Kotlin 目前已经毫无障碍了。那么作为 Android 开发者,对比 Java 而言,使用 Kotlin 语言能获得什么好处呢?

第一点,使用 Kotlin 能够让我们的代码变得更简洁。我们都知道 Java 以它语法的严谨性而著名,它支撑起了世界上各种大型、复杂的计算机软件。然而 ,它的语法是有历史包袱而且略显啰嗦的,对比更灵活的现代语言,使用 Java 来实现同样的逻辑通常需要写更多的代码。而没有历史包袱的 Kotlin 则吸取了现代语言各种灵活简洁的语法,让开发者在 JVM 上也能写出简洁的代码:

// Java
final ArrayList<String> a = new ArrayList<>();
// Kotlin
val a = ArrayList<String>()

// Java
public String b(String c) {
    return "Test: " + c.substring(2);
}
// Kotlin
fun b(c: String) = "Test: ${c.substring(2)}";

除了语法上的各种简便,Kotlin 的标准库也提供了很多实用的方法来精简你的代码,例如针对开发中最常用的「集合」,Kotlin 提供了封装让你可以轻松创建集合类、使用和 Java Stream 相似但更丰富的接口来操作集合:

// 创建包含元素 1、3、5、7 的 ArrayList
arrayListOf(1, 3, 5, 7)
        // 过滤出集合中大于 3 的元素
        .filter { it > 3 }
        // 转换成字符串
        .map { "$it, " }
        // 循环输出
        .forEach { println(it) }

更多 Kotlin 比 Java 语法更精简的例子可以查看 From Java to Kotlin

第二点,使用 Kotlin 能让你的程序更安全。Java 工程师最常见陷阱之一就是访问了空的引用而导致空指针异常,而 Java 在语法上无法描述某个引用是否可空,所以开发者要背负起更多的心智负担而不得不经常进行判空操作。虽然目前可以通过 Annotation + IDE 提示的方式来一定程度上减轻这个负担,但这并不是一个强约束,在 IDE 上的提示是可被忽略的。

而 Kotlin 在语法上对此进行了强约束。在 Kotlin 中定义某个引用时必须描述其是否为可空类型,对于可空类型引用的不安全访问会在编译期报错:

// 不可空类型,可以直接访问
val a: String = ""
a.substring(2)

// 可空类型
val b: String? = null
b.substring(2) // 不安全访问,编译报错

这有利于在运行前察觉并处理可能的空指针异常。而且 Kotlin 还提供了 ?.?:、类型自动转换等便捷的语法来辅助处理可空类型:

val b: String? = null
b?.substring(2) // 当 b 不为空时才调用 substring()
if (b is String) {
    b.substring(2) // 自动把 b 转换成不可空类型
}
val c = b ?: "" // 如果 b 为空的话,则返回 ?: 操作符右边的值

以上两点是 Kotlin 能给大多数 Java 开发者带来的直接好处。但 Kotlin 能给开发者带来的也不仅仅只有这些,它有着完美的 IDE 支持(这也正是 JetBrains 的强处),它与 Java 之间的高互操作性让你可以轻松使用 Java 生态中丰富的库,而它对各种现代语言新特性(例如协程、函数式编程)的支持,能让你在面对不同的计算机问题时有更多不同的思考和解决方式。

必须知道的一些缺点

我们要知道,任何编程语言在设计时都需要做各种取舍。Kotlin 在提供高灵活性的背后也需要开发者付出一定的代价:

  • 语法糖过多,加重开发者心智负担;
  • 部分语法糖,例如 Extensions,会破坏代码的可阅读性;
  • 灵活性过高,不同的开发者容易产生不同的表达偏好;
  • 过度依赖 IDE,脱离 IDE 可能让代码难以阅读和维护。

实际上这也是大多数语法灵活、语法糖多的语言的共同问题。但我们不必过于担心,通过使用强大的 IDE 和建立代码规范这些问题都能被解决。作为开发者,我们应该把精力放在用更少的代码、更快、更方便地描述出我们想要的「逻辑」上,其他的负担都交给编译器或 IDE 吧,这也正是高阶编程语言诞生的初衷。

作为 Android 开发者是否应该学习 Kotlin?

总所周知,由于 Google 和 Oracle 之间的各种政治斗争,导致 Android 开发者一直以来只能用上阉割版的 Java。大部分开发者甚至是最近几年才开始用上、甚至开始知道 Lambda 表达式,而像 Stream 之类的工具更是无人知晓。虽然期间有传闻过要支持 Go 和 Dart 语言,但很快也都音讯全无了。

而 Kotlin 的出现正好弥补了 Android 开发生态中一块巨大的短板 —— 落后的开发语言。而且,Kotlin 和 Java 十分相似(甚至很多人把它认为是 Java 的增强版),所以从 Java 过渡到 Kotlin 的门槛比起其他语言来说相对更低。而基于 JVM 又让 Kotlin 的代码可以很轻松地运行在 Android 平台上。这么看来,Kotlin 确实比起 Google 自己的 Go 和 Dart 来说更适合作为 Android 平台的开发语言,也难怪 Google 最终敲定 Kotlin。

纵观未来,随着 Google 和 JetBrains 深度的合作,Kotlin 也肯定会成为 Android 开发生态中最先进的工具之一。目前通过 Kotlin Android Extensions 已经可以很方便地在 Activity 中直接通过 Id 名来直接访问对应的 View:

import kotlinx.android.synthetic.main.activity_main.*

// 设置 id 为 helloTextView 的 TextView 的文本
helloTextView.text = "Hello World!"

而 Google 官方推出的 KTX 库 更是让开发者能够更方便地使用 Kotlin 来开发 Android 应用:

// 使用 KTX 前
view.viewTreeObserver.addOnPreDrawListener(
    object : ViewTreeObserver.OnPreDrawListener {
        override fun onPreDraw(): Boolean {
            viewTreeObserver.removeOnPreDrawListener(this)
            actionToBeTriggered()
            return true
        }
    }
)

// 使用 KTX 后
view.doOnPreDraw {
     actionToBeTriggered()
}

另外一个好消息是,今年 11 月刚发布的 Gradle 5.0 也宣布支持了 Kotlin DSL,这意味着我们甚至可以用 Kotlin 来写我们的构建脚本了:

android {
    compileSdkVersion(27)
    defaultConfig {
        applicationId = "com.test.app"
        minSdkVersion(15)
        targetSdkVersion(27)
        versionCode = 1
        versionName = "1.0"
    }
}

所以从各种迹象来看,答案其实已经很明显了。Kotlin 的诞生以及被 Google 的钦点,对一直以来被语言限制生产力的 Android 开发者们而言意义非凡。而就 Google 和 JetBrains 的影响力来看,未来几年 Kotlin Android 开发者的数量将呈爆炸式增长,市场对于 Kotlin 工程师的需求也将会不断增加。所以,学习 Kotlin 不但能让你接触到更先进的工具、思想,也肯定能让你在人才市场上更具竞争力。

事实上,根据 Github 今年发表的 Octoverse 报告 ,Kotlin 已经成为增长速度最快的语言。

先尝试一下吧

如果你已经对 Kotlin 产生兴趣,可以先通过官方的 Playground 来在线尝试下 Kotlin 的语法。它还包括一些列用于演示各种语法的实例,以及一个完整的语法课程。

如果你想在本地创建一个全新的使用 Kotlin 编写的 Android 应用项目,参照官方文档中的 Getting started with Android and Kotlin 来进行即可,目前 Android Studio 已经完全支持 Kotlin 语言。

而如果你想在一个使用 Java 的 Android 应用项目中同时使用 Kotlin,也是完全没问题的。通过上一个链接的教程引入 Kotlin Gradle Plugin,即可在你的源码目录下通过 Android Studio 菜单直接创建 Kt 源码文件。

有人可能会担心使用过程中遇到各种坑 。实际上,笔者在 2015 年就开始使用 Kotlin 了,期间在语法、IDE 支持、Kotlin 注解处理器上都遇到官方不少的坑,但由于官方的迭代速度足够快,很多问题很快就被修复了。另外,Kotlin 的社区也十分活跃,如果遇到坑或者问题也基本都能在上面找到回答。

而自从 16 年 Kotlin 1.0 发布之后的版本就更加稳定了,工具链、IDE 支持也都十分完善。所以大可不必担心会遇到无法解决的坑。

最后说点什么

有科学家表明,使用不同的自然语言会影响人的思考方式。而编程,亦是如此。激进的 Kotlin 和保守的 Java 之间的差异,肯定会给我们带来不一样的思考问题的方式。而这些不一样,也肯定会影响未来 Android 开发的新风向。

在笔者看来,Android 开发界随着 Kotlin 的出现实际上已经到了一个新的纪元,浪潮已来,为了不被浪潮所击退,请用力拥抱 Kotlin 吧!

♥ 28💬 4
想法2018-12-25

曾遇到过面试途中应聘者接电话而导致面试被单方面打断,而且接电话前后都没对中断了面试谈话而表示抱歉,作为谈话的另一方确实感觉到了没有被尊重。这其实就是个礼貌的问题,面试中不能接电话当然不是必须的,但也不要觉得反过来是理所当然的。

♥ 0🔁 0💬 0
想法2018-12-17

最近在向一位做社区分享 & 教育的谷歌开发专家学习,深刻意识到一点:向大众传播知识时必须得放低姿态、尊重中尾部读者。

♥ 0🔁 0💬 0
想法2018-12-11

李明亮等100万人:

♥ 0🔁 0💬 0
想法2018-12-05

十分赞同文章回复里说到的 careless driven programming,坏的 codebase 带来的维护成本是远高于开发成本的,函数式编程正是用前期更高的心智负担来换取后期的 careless 阿莱克西斯: 脱离具体计算机体系构架的实现而发明更可拓展更便利的抽象,是正确的程序设计发展思路。最好的例子就是干掉goto和发明了for和while loop,遵循“计算机的本源”那我们是不是应该大量写goto? 不要开历史的倒车呀 —_—

♥ 0🔁 0💬 0
文章2018-12-02

被滥用的 GUI 设计模式

原文来自 被滥用的 GUI 设计模式

随便侃些个人对 GUI 设计模式的看法。

近些年来,随着 Fronted 技术的火热和推进,古老的(至少有几十年历史)用来解决 GUI 应用中代码组织问题的「GUI 设计模式」现在也成为了 Frontend 工程师的热门话题,MVC、MVP、MVVM 等设计模式在网路上被议论不绝。有很多工程师开始通过写博文来介绍它们、阐述自己对它们的理解,甚至在 Github 上开源了各种 GUI 设计模式的实现。

顺着这种趋势,很多 Frontend 工程师甚至把 GUI 设计模式当成一种「规范」乃至「教条」。然而糟糕的现实是,大多数人并没有正确地、细致地理解和运用 GUI 设计模式,反而因为 Tradeoff 导致它的缺点被放大。结果就是你用了大量精力、模板代码去设计它,反而让它更复杂、更难维护了。

例如,当你打开 Github 上大多数试图实现 GUI 模式的仓库时会发现,整个应用大概也就两三个页面、四五个网络接口,就可能已经创建了几十个类和接口来承载那单薄的逻辑了。举个更具体的例子,我个人曾经接触过几个用 MVP 模式设计的大型 Android 工程,在进行维护或者迭代的时候,各种带有问题的设计反而让 MVP 模式成为了累赘。

首先,工程中大多数 View 都是粒度大耦合度高的 Activity 类,而且很多 View 里为了方便,会提供 fun updateView(user: UserModel) 这样的方法,导致 View 和领域/业务模型直接耦合了。再者,View 和 Model 中还会包含了跳转页面、发送全局消息等各种带有「副作用」的命令,这也让面向接口编程成为了形式主义。

所以与其「舍本逐末」、「知其然而不知其所以然」,倒还不如理解问题的本质。于 GUI 设计模式而言,实际上最重要的思想是「分而治之」,通过把之前都写在一处的代码按照职能分到不同的类,来让它们实现「低耦合高内聚」。所以,我们更应该把 GUI 设计模式当成一思想而不是具体的手段,更也没必要用各种所谓的模板来解决问题,只要你能把热点、关键代码设计得足够低耦合高内聚,那么你完全可以无视所有 GUI 设计模式。

例如上面提到的 fun updateView(user: UserModel) 问题,实际有两种方式来让 View 和业务模型 UserModel 解耦:

// 方法一
interface ViewA {
    fun updateText1(text: String)
    fun updateText2(text: String)
    // ...
}
class PresenterA {
    fun onSomeEvent() {
        val userModel = Apis.requestUser()
        viewA.updateText1(userModel.name)
        viewA.updateText2(userModel.age.toString())
    }
}

// 方法二
interface ViewA {
    data class ViewAttributes(
        text1: String,
        text2: String,
        // ...
    )
    fun updateView(view: ViewAttributes)
}
class PresenterA {
    fun onSomeEvent() {
        val viewAttributes = Apis.requestUser().mapTo ViewAttributes()
        viewA.updateView(viewAttributes)
    }
}

方法一更倾向于用「指令」来描述 View,方法二则更倾向于用「数据」。而我个人更喜欢方法二,因为数据是运行时可处理、可持久化的,甚至可以跨进程、跨语言、乃至跨机器共享的。讲个题外话,Web Fronted 里 Redux 等状态管理工具捧起了一个很火的词「时间旅行」。在我看来核心思想其实也是把指令下沉,用数据(/状态)来描述上层逻辑,这样就可以在运行时实现逻辑可记录、可回放。

这里还有一点需要注意的,ViewAttributes 必须是 View 的领域模型,字段名称应当仅和 View 本身相关,而不应该和其他领域有关系。

再回到之前提到过的另外一个问题:View 和 Model 里的副作用。这个其实更容易解决,只需要把所有副作用移到外部(/调用方)就好了。例如:

// 有副作用
class ViewA {
    fun onTitleClick() {
        sendBroadcast("x")
    }
}

// 无副作用
class ViewA {
    fun onTitleClick() {
        caller.onTitleClick()
    }
}
class PresenterA {
    fun onTitleClick() {
        sendBroadcast("x")
    }
}

实际上,只懂得 OOP(面向对象编程)的工程师很容易造成前面提到的问题,因为他们习惯了依赖「外部状态」来解决问题(类的实例本身也是一个状态),但是在状态数量不断增加的情况下,状态的管理反而会成为一个新的大难题。而 OOP 提倡类的「低耦合高内聚」实际上可以看成是在解决状态管理的问题。

所以在文章的最后,我强烈推荐工程师们可以学习下 FP(函数式编程)。相对于 OOP 而言,FP 的思想则是摒弃外部状态,它实现的是粒度更小的函数级别的「低耦合高内聚」,你只需要保证你的函数是无副作用的然后管理好函数内部的状态就可以了。而维持这种编程思想,能让你轻松驾驭巨型、复杂的项目,甚至能让你的代码更容易被调试,更容易被并行执行。

对于 Android 工程师们来说,Kotlin 目前的火热正是让大家有了更了解 FP 的机会。之后我也会写些和 Kotlin、FP 有关的文章。

♥ 41💬 7
想法2018-11-28

评论里真的是我近期来看到最有趣的互杠,某伪 v 粉居然还有这样不懂装懂的高级姿势?(连源码都扯出来了…但终究连 fiber 是个啥都不知道

🔗 react是不是比vue牛皮,为什么?

♥ 0🔁 0💬 0
想法2018-09-21

慢了一小步哈哈 [摊手]

♥ 0🔁 1💬 2
回答2018-09-11

作为安卓工程师,你最希望 Android 平台解决哪些技术痛点?

提一个,Android Studio 自带的 Profiler 还不够好用,例如:

  • Method Tracing 完之后没法中断 Monitor,导致越来越卡
  • 自带打开 Trace 文件的功能,可视化很烂,基本没法用,而且没法用 Profiler 打开
  • 感觉 Profiler 的 Method Tracing 这块有些地方做得没之前的 TraceView 好
  • 等等
♥ 0💬 4
文章2018-07-19

知乎安卓客户端启动优化 - Retrofit 代理

背景

知乎 Android 客户端作为一个比较大型的应用,由于功能不断地迭(zeng)代(jia),启动速度也会受到影响,为了提升用户体验,知乎移动平台团队把提高 App 启动速度定为了的一个长期而且重要的 OKR,于是我们在今年的第二季度,重点对客户端的启动做了一系列的优化。

虽然在性能优化相关领域我们还处于刚开始的阶段,但是在优化过程中我们还是总结出了一些经验可以拿出来分享。所以,今天我们来分享其中一次和 Retrofit 相关的优化经历。

开始之前

我们在做性能优化时,很多人可能苦恼于怎么去检验或者说量化最终优化的效果。这里面其实是有学问的,通常我们会选用系统输出的信息来作为指标,例如众所周知的用 GPU 渲染柱状图来作为 UI 是否卡顿的指标。为什么要选系统输出的信息?一来采集更方便,不需要自己写代码去测量,二来是我们还能采集到其他 App 的信息,方便与其他竞品做横向对比。

而启动速度的话,我们会选用系统打印的 Activity 启动 Log 作为指标:

107-11 15:09:32.519 1440-1502/? I/ActivityManager: Displayed com.zhihu.android/.app.ui.activity.MainActivity: +1s412ms (total +1s978ms)

Log 数据的含义可以看 Stack Overflow 上的这个 回答 ,我们主要取 App 冷启动到第一个 Activity 显示出来的时间。

言归正传,在这次优化开始之前,为了能和最终的数据做对比,我们需要先测出 App 优化前的启动时间。通过对 App 进行多次冷启动并记录 Log 里的 Total duration,得出了 App 在优化前的平均启动时间为 1.905s(数据来自 OnePlus 3T):

Log 输出

发现 & 分析问题

我们想要找究竟是哪些代码导致启动变慢,光靠 Review 代码是做不到的,因为一段代码的执行效率除了受各种内因(CPU / IO 操作密集,锁…)的影响,还有可能受其他外因(系统资源争夺、GC…)的影响。所以想要快速准确地找出问题,最好的方法是让代码真正执行起来,然后去 Profile(监测)代码运行时的情况。

而 Profiling 其实是一门很大的学问,需要用到大量的工具,包括 Android 系统、SDK、甚至 IDE 提供的一些接口或工具,熟练运用这些工具去分析和发现性能问题是做性能优化的必备技能。而针对今天知乎 App 启动的 Profiling,我会简单介绍其中一部分的工具:

Method Tracing

Method Tracing,就是跟踪 App 某段时间内所有调用过的方法,这是测量代码执行性能常用的方式之一,我们可以通过它来查出 App 启动时具体都调用了方法,都花了多长时间。

这个功能是 Android 系统提供的,我们可以通过在代码里手动调用 android.os.Debug.startMethodTracing() 和 stopMethodTracing() 方法来开始和结束 Tracing,然后系统会把 Tracing 的结果保存到手机的 .trace 文件里。详情可以看 官方文档

此外,除了通过写代码来 Trace,我们也有更方便的方式。例如也可以通过 Android Studio Profiler 里的 Method Tracer 来进行。但是,针对 App 的冷启动,我们则通常会用 Android 系统自带的 Am 命令来进行,因为它能准确的在 App 启动的时候就开始 Trace:

# 启动指定 Activity,并同时进行采样跟踪
adb shell am start -n com.zhihu.android/com.zhihu.android.app.ui.activity.MainActivity --start-profiler /data/local/tmp/zhihu-startup.trace --sampling 1000

当 App 冷启动完毕后(首个 Activity 已经绘制到屏幕上),使用以下命令手动终止跟踪,并拉取 .trace 文件到本机的当前目录下:

# 终止跟踪
adb shell am profile stop
 
# 拉取 .trace 文件到本机当前目录
adb pull /data/local/tmp/zhihu-startup.trace .

拿到 .trace 文件之后,下一步就是进行可视化了。可以直接拖动 .trace 文件到 Android Studio 里打开,但 Android Studio 目前版本对 .trace 文件的可视化和交互做得还不怎么样,所以不推荐。在这里推荐使用 Android Device Monitor 里的 Traceview 来打开,详情可以查阅 官方文档

用 Traceview 打开之后的截图:

Traceview 截图

所有跟踪到的方法会默认按照实际总耗时从多到少向下排序,点击某个方法可以看到它的所有父调用方法和子调用方法。通过从上往下一条条排查,可以看到在知乎 App 启动时 UserInfoInitialization 类的 initUserInfo() 方法竟然耗时超过 600ms:

initUserInfo() 方法

159 (0x68f8) 方法

我们来看看这个 initUserInfo() 方法的代码:

private void initUserInfo() {
    NetworkUtils.createService(ProfileService.class)
            .getSelf(AppInfo.getAppId())
            // ...
            .subscribe(response -> {
                // ...
            }, Debug::e);
}

NetworkUtils.createService() 是我们自己封装的一个方法,内部调用 Retrofit 来获取 ProfileService 的动态代理类,而 getSelf() 则是动态代理类提供的一个方法。从 Traceview 中可以看到,159 (0x68f8) 这个是运行时生成的代理方法,所以可以判断这里的耗时是因为调用 getSelf() 引起的。

继续往下跟踪:

responseBodyConverter() 方法

_newReader() 方法

可以看出来,最终主要的耗时在 Jackson 库的几个方法上,而 Jackson 库是我们是用来给 Retrofit 序列化和反序列化数据(例如 Response Body)用的。

接下来就是深入追查原因了。通过查看 Retrofit 的代码可以知道,在第一次调用 Retrofit 的某个动态代理方法时,Retrofit 会新建一个 ServiceMethod 实例来储存该代理方法相关的一些数据,包括一个用来转换 Response Body 的 Converter。而在新建这个 Converter 的时候,则会根据 Body 反序列化结果对应的 Java Model 类来生成一个 ObjectReader,如果对应的 Java Model 类特别复杂,那么新建 ObjectReader 的时间也会特别长(内部会进行一堆反射操作)。而恰恰我们 getSelf() 方法返回的 Java Model 是一个字段极其多的类,所以造成第一次调用该代理方法时特别耗时。

问题的原因我们已经通过 Method Tracing 找到了,接下来我们可以通过另外一个工具来看看问题发生时,App 和系统的真实情况。

Systrace

Method Tracing 虽然是找出耗时方法的利器,但是执行 Method Tracing 时会严重拖慢 App 的执行速度,即便使用采样跟踪,测量得到的结果和实际结果肯定还是有很大偏差,只能作为参考。而且 Method Tracing 对锁、GC、资源匮乏等其他因素的追查显得十分无力。所以,我们可以借助另一个 Google 官方极力推荐的工具 -「Systrace」来跟踪 App 实际运行时的情况。详细介绍可以查看 官方文档

Systrace 的原理是通过 Android 系统自带的 atrace 工具来捕获系统以及 App 的一些关键信息,然后通过 Chrome 浏览器来进行可视化。

接下来,我们将通过 Systrace 来跟踪知乎 App 在启动时执行 Retrofit 代理方法的整个过程。首先,我们需要做一些准备:

  1. 为了让结果更接近真实情况,我们需要为 Release 包开启 App Tracing 的功能,详情可以查看 这里
  2. 给 Retrofit 的 loadServiceMethod() 方法添加 Tracing Section(PS:可以借助 JarFilterPlugin 来修改 Retrofit 的内部代码):
ServiceMethod<?, ?> loadServiceMethod(Method method) {
    Trace.beginSection("Retrofit:" + method.getName());
    // ...
    Trace.endSection();
    return result;
}
  1. 添加 Proguard 规则,保证 method.getName() 获取到正确的方法名:
-keepclassmembernames class * {
    @retrofit2.http.GET *;
    @retrofit2.http.POST *;
    @retrofit2.http.PUT *;
    @retrofit2.http.DELETE *;
}

做完准备后用 Gradle 命令打出一个上线标准的 Release 包并安装上手机,然后就可以开始用 Systrace 来跟踪了:

systrace -a com.zhihu.android app view res am sched dalvik

开始跟踪后,冷启动 App,等到首个 Activity 可见之后点击回车结束跟踪。跟踪结束后 Systrace 会把跟踪结果保存到当前目录下的 trace.html 文件,使用 Chrome 打开后:

Chrome 截图

通过 Chrome 可以很直观地看到 App 在启动时的各种关键的 Section。此外,我们还能看系统 CPU 每个时期的使用率以及每个核心上执行的线程:

CPU 使用率和每个核心上执行的线程

通过选定某个线程上面的线程状态条,我们可以查看某段时间或 Section 里线程的运行状态:

线程状态

从上图可以看出,在「Retrofit:getSelft」这个 Section 里,UI Thread 基本都是 Running 状态,说明 getSelft() 内部执行的都是 CPU 密集的操作,很少进入等待状态。这也说明了 getSelft() 方法的耗时是实打实的,不是由于其他原因(例如锁等待)造成。而且有一点需要注意,这里测出来的实际耗时是 250 多毫秒,而 Method Tracing 测量出来的耗时是 650 毫秒,由此可以看出 Method Tracing 的测量结果误差还是很大的。

接着我们通过搜索「Retrofit:」关键字可以看到,在启动期间会不止一次调用到 Retrofit 的 loadServiceMethod() 方法:

搜索结果

而且有很多还是在 UI 线程上调用的,但是其实这些操作没必要放在 UI 线程上,我们需要想办法把所有 loadServiceMethod() 的调用都扔到其他线程上。

解决问题

耗时的原因已经找到了,而优化的思路就是把 loadServiceMethod() 的调用扔到非 UI 线程上。例如,针对 getSelf() 这个方法,我们可以使用 Observable 的 defer() 操作符把它放到非 UI 线程上去异步执行:

private void initUserInfo() {
    Observable.defer(() ->
            NetworkUtils.createService(ProfileService.class).getSelf(AppInfo.getAppId())
                    .subscribeOn(Schedulers.io())
    )
            .subscribeOn(Schedulers.single())
            // ...
            .subscribe(response -> {
                // ...
            }, Debug::e);
 
}

这是针对单处地方的改法,但要知道,启动的时候不单单只有这一处会调用到 Retrofit 的 loadServiceMethod(),要把所有地方都加上 defer() 操作符的化修改量有点大。所以有没有更好的办法呢?

答案就是二次动态代理:

public final class Net {
    public static <T> createService(Class<T> service) {
        // ...
        return createWrapperService(mRetrofit, service);
    }

    private static <T> T createWrapperService(Retrofit retrofit, Class<T> service) {
        return (T) Proxy.newProxyInstance(service.getClassLoader(),
               new Class<?>[]{service}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, @Nullable Object[] args)
                throws Throwable {
                if (method.getReturnType() == Observable.class) {
                    // 如果方法返回值是 Observable 的话,则包一层再返回
                    return Observable.defer(() -> {
                        final T service = getRetrofitService();
                        // 执行真正的 Retrofit 动态代理的方法
                        return ((Observable) getRetrofitMethod(service, method)
                                .invoke(service, args))
                                .subscribeOn(Schedulers.io());
                    })
                            .subscribeOn(Schedulers.single());
                }
                // 返回值不是 Observable 的话不处理
                final T service = getRetrofitService();
                return getRetrofitMethod(service, method).invoke(service, args);
           }
           // ...
        });
    }
}

原理就是再创建一层动态代理,然后把底层 Retrofit 代理方法的调用包进新的 Observable 里再返回。这么改的好处是可以一劳永逸,让所有调用的地方无需做任何更改,减少了代码的修改量。

经过这么优化之后,我们重新测试了一遍启动时长,最终得出优化后的启动时间大概快了 400ms。可以看出,这次优化是成功的。

结语

我们这次优化经历到这里就结束啦,可以看到我们整篇文章里的「发现 & 分析问题」这一章占的篇幅是最大的,这其实也反映了我们在做性能优化时的常态,大多数时间都会花在 Profiling 上,当遇到优化瓶颈时,更可能花大量时间去通过各种工具抽丝剥茧地分析问题。

其实上面也讲到了,Profiling 是一门大学问,本次分享只是挑了些比较简单但是关键的方式和工具来讲,实际在进行 Profile 的时候会用到更多的知识,有时候甚至需要自己写工具去辅助定位问题。希望本篇文章能起到抛砖引玉的作用,让大家能了解到做启动优化时的一些常用思路。此外,该次只是我们优化知乎 Android 客户端启动速度的其中一次经历,知乎移动平台团队还在不断地为优化启动速度做努力~也希望未来还能分享更多的经历给大家。

由于本人的水平有限,如有错误和疏漏,欢迎各位同学指正。

另外,知乎移动平台团队也在招人中,欢迎各位小伙伴的加入,和我们一起做一些酷事情!具体招聘信息在这里 https://app.mokahr.com/apply/zhihu#/job/7b1b32c2-f30c-4638-93ce-09c2ac9a52d8

♥ 176💬 32
想法2018-07-19

订阅了 JetBrains 全家桶一年,也算是支持了下这家良心企业(手动狗头

♥ 0🔁 0💬 4