如何成为一名进阶的Android开发者
*本文Notion weblink。欢迎直接评论,提出宝贵建议:
【资料图】
https://learned-stay-51d.notion.site/Android-960b53ff4af2496bbf55a4f932ae4beb
阅读时长大约30分钟
前言
文章内容原为我在前公司开的一门内部课程,面向刚刚进入公司,从事Android开发校招生。做了内容脱敏后分享在这里。
写这篇文章的初衷是希望能让读者更好的进入Android开发者的角色。比起市面上五花八门的Android视频课程,这篇文章不会聚焦在技术的使用和实现上,而是更多的是集中在以下几个方面:
如何去了解项目使用的语言、组件、架构、代码风格等
使写出来的代码有章法。满足可靠、可拓展、可维护这样的原则
如何调试代码,当执行遇到问题时使用合适的工具排查
一些Tricks,可以单位时间内产出更多,下班更早(Maybe)
简单的自我介绍
从事Android开发八年,大厂、外企都呆过,既从零搭建过很多新项目,也维护过很多日活千万的应用。
To be a developer for Android
如果你跟我一样,在上岗成为Android开发者之前,并没有系统的学习过Android开发,那么经常翻阅Android门户网站(https://developer.android.com/)是一个非常好的习惯。里面包含但不限于:
入门开发课程(十分推荐新手们按照课程实操一遍)
API Docs
一些官方推荐的第三方库
Android OS最新版本Features
App上架Play Store的政策
你可能已经习惯开发中遇到问题时,通过Stack Overflow等渠道,描述问题的指征,并寻找靠谱的答案。这里建议你不妨转换下思路,下次遇到类似的情况时,先尝试翻翻API docs等,有些比较基础的问题可能早已经写在doc中了。
更重要的是,在查阅doc的过程中,你会阅读到一些上下文信息(比如这个类的其他API、用法和注意事项),相较于之前寻找答案的方式,你可能会获得许多额外的知识,这会更有助于你了解一个类、一个组件、一个框架的全貌,闻一而知十。
Android Studio
这是你打开Android Studio后看到的景象。默认设置下,整个IDE会被划分为这五个区域:
左侧:树形结构的方式展示项目的文件
底部:一些编译、运行时的监视窗口,命令行,性能监控等
右侧:Gradle Tasks列表,模拟器,布局预览等
顶部:一些功能的快捷入口,比如编译、Git等
中部:代码编写区域
如果屏幕不是特别大,可以按工作需要展开、折叠部分区域,以突出重点,节省视力。比如Coding时一般只需要代码编写区域和左侧的项目结构图了;画UI时除了Coding的区域,还需要增加右侧的布局预览;编译时,底部的监视窗口用于观察编译速度,编译错误时分析原因;运行时底部的Logcat用于看日志内容。
熟悉项目文件结构
如果你是接手一个现成的项目,执行完git fetch
,导入整个项目,在撸起袖子干之前,不妨先熟悉整个项目的文件结构。这有助于你后续工作中快速定位到对应的位置,修改代码。
项目中各种文件的存放位置一般遵循以下规则:
如果项目包含子模块(Sub Module),以上规则同样适用于各子模块。
熟悉项目文件结构的过程中,我们同样可以试着回答以下问题:
Java层代码主要使用的是Java还是Kotlin?
异步框架使用的是RxJava、Coroutines还是传统的Handler、Executer?
有没有使用Sub Module划分模块?代码按照什么规则隔离分层?
Maven Dependencies有依赖哪些热门组件?
有没有使用Router?Dagger?AOP?插件化?
类似的还有很多。随着经验的积累,你所能提出的问题就越多,越有助于你与整个开发团队的节奏保持一致,避免写出不符合团队风格的代码。
Gradle
Android的编译构建工具,也就是Gradle,它的语法基于Groovy,是一种DSL(Domain Specific Language)语言。它既可以用来写构建的各种配置(Maven依赖、本地Projects相互依赖、插件依赖),也可以编辑调整构建的任务流程,亦或者用来写一些简单的可执行逻辑。
Gradle是一个很容易劝退新手的地方。随着扩张,一个项目的Gradle文件往往会变得愈发冗长复杂,内部充斥着各种依赖、执行逻辑,让人无法轻易理解。这里我们不妨先简单的了解下Gradle的一些基本概念,好在后续阅读Gradle文件时有个大致的头绪。
Gradle首先可以用于配置参数和一些依赖。例如上面的Gradle代码,用于配置Android项目编译的各种依赖项——Maven仓库地址、编译版本。下面是我将这些Gradle代码类比成了Java代码的形式,方便理解,它们等同于一系列的setter
和import:
其次,Gradle中也可以写一些简单的可执行逻辑。例如上面的给到的一个函数,当某个文件无法被找到时,通过命令行去执行一个python脚本。这个函数可以被其他Domain调用执行。
最后,Gradle的构建任务由两个基本单元组合而成:
Project
Task
一个Android App项目至少拥有一个Project,一个Project中可以包含(1..n)个Tasks。它们相互之间的关系可以用下面这张草图来描述:
Project是Gradle编译执行的交互接口。你可能没有直接接触过它,但你肯定见到过build.gradle
文件。一个build.gradle
文件就是对一个Project的定义和实现。项目中有几个build.gradle文件,就代表了有几个Project。其中,项目根目录下的build.gradle
作为整个App编译的入口。
Project也可以存在依赖关系。其中,项目根目录下的settings.gradle
文件用于告知Gradle,这个App Project需要编译哪些Projects。
而非根目录下的Project(Android项目中称作Module),它们相互之间亦可以存在依赖关系。它们的依赖关系直接定义在其build.gradle
的dependencies domain中
Task是Gradle编译的最小单元。如它的名字一样,Task为整个编译过程拆分后的一个个单元“任务“。它会从上一个Task中获取Context,做点什么,(可能)修改一些Context中的内容,最后将Context传递给它的下一个Task。整个流程近似一条流水线(pipeline)一样。
那么Task的执行顺序是如何决定的呢?这里引申出一个可能会令人困惑的点:它们的执行顺序由它们的依赖关系决定的。不通于使用控制逻辑if-else
、switch-case
直接控制程序的执行逻辑,Task间只能通过dependsOn
来决定它们的依赖关系,从而间接的控制它们的执行逻辑。形如:
这样Gradle的执行顺序就会是taskA → taskB
让我们玩的稍微花一些:Task同样可以一对多,或者多对一的依赖。
那么最终形成的依赖关系,和最重Gradle的执行顺序,大概是:
Task需要依赖于Project,所以定义Task和依赖关系,需要在build.gradle
中处理。
UI Components & Data Sources
Android开发(可能)大部分的工作都集中在将数据呈现在UI。自嘲高级UI工程师的同时,UI与数据间的交互方式却从来没有停止进化的脚步。
第一代交互方式。先在xml中绘制出UI的布局,然后在Activity
中渲染,最后通过findViewById
定位到需要操作的View
,通过手动调用各种Setter和Callback来做到UI和数据的双向绑定。
Databinding伴随着MVVM模式的走红成为了第二代交互方式的代名词。我们可以在定义完UI对应的数据结构对象后,在xml中直接将它们绑定在对应的UI控件上。编译过程中,每个引入Databinding的xml都会自动生成其对应的Binding类(build/generated/data_binding_base_calss_source_out),其中有自动生成的UI与数据的双向绑定代码。
可以说Databinding极大的简化了高级UI工程师们的工作量,并且跟易于后续的维护工作。你可以避免出现类似数据更新后,忘记手动调用Setter方法,从而没有刷新UI的Bug。
Google近期大力推广的Compose有机会成为第三代交互方式。
Compose出现之前,Android的视图是树形结构。更新视图需要手动操作树形结构中的节点,有的情况下还需要调整树形结构本身(addView
、removeView
),官方称其为“命令式视图”。
Compose意在以声明式UI来进一步减少通过DataBinding实现响应式UI的代价。举个例子,通过DataBindingUI和数据的双向绑定时,一般有三个步骤:
Layout:画UI布局
Compose:将一般数据组装成LiveData
包裹的可观察数据
Bind:在布局文件中将可观察数据与View绑定
而Compose带来的响应式编程直接将这三个步骤平铺在了一起,画布局、绑定可观察的数据、包装数据这些工作可以混合在一起处理。
原先你可以直接构建一个View
、ViewGroup
对象,添加到UI树中,并且在运行时可以随时调整它的各种属性,甚至随时将它从UI树中移除。
但是Compose中,你已经不能这么干了。你无法直接接触到各种View
对象,进而无法调用它的各种方法,更别说随意的从UI树中移除。你只能调用各种UI Widget对应的@Composble
静态函数来声明你要创建的布局或者UI组件,同时将布局或UI组件的属性绑定到对应的可观察数据(State
),后续对UI的控制也只能通过更新State
来实现。
初步体验下来,Compose有一定的学习曲线,代码整体风格更偏向于前端,而对可观察数据需要有更强的抽象能力。它同时将DataBinding的理念贯彻到底,直接杜绝了声明UI后再直接操作UI的可能性。所有的UI刷新都是通过更新State
,或者更加灵活的冷流Flow
,最终由Compose框架决定是否需要更新UI,以及更新哪些部分的UI,以牺牲自由换取更高效率。
Third-part Dependencies
大到搭建一个App项目,小到完成一个小需求,这些工作都不是从0开始的。琳琅满目的第三方依赖库能够帮助你节约开发时间、提升开发质量,从而将你大部分的精力集中在业务本身,毕竟你不是总有那么足够的时间去造一个个轮子(多数时候造的还不那么好)。
使用第三方依赖库的一些要注意的点:
安装包体积:只是为了使用一个很小的功能点,却要引入一个体积庞大的依赖
间接依赖冲突:第三方依赖库A和B都间接依赖了C,但是依赖C的版本不同。编译时产生冲突,或者可以编译但是运行时抛出异常
废弃后带来的问题:如果作者不再维护它了,Bug-fix和一些升级所带来的适配没有人来处理
遇到这些情况时,你可能就得为之前的便利还一些技术债。fork一份源代码做修改,或者不得不自己重新造一个轮子。
架构、架构模式、设计模式
这三个概念经常会被混淆。从宏观向微观的顺序来看:
架构:用于描述系统的组件栈、组件之间的层次和依赖关系,它只是一个草图,所以我们没有办法通过只阅读架构图来了解系统的细节,或者进行具体的实现
架构模式:也就是我们常听到的MVC、MVP、MVVM、MVI。它负责定义各个模块的职责,起到一个解耦的效果。同时它也会定义各个模块间的通信方式,以及信息的流动方向
设计模式:特指24种设计模式。人类的本质是复读机,你所遇到的问题,绝大多数情况下别人也遇到过,设计模式就是前人留下的这些特定问题的优解
所以回到Android开发者,一般情况下我们是不会接触到架构,更多的时候是跟架构模式、设计模式打交道。
我们为什么需要这些条条框框呢?《Designing Data-Intensive Applications》一书提出了我们耳熟能详的优秀代码三要素:可靠、可拓展、可维护。我对这三点做了些自己的理解:
可靠:不容易出Bug
可拓展:不容易改出Bug
可维护:别人不容易改出Bug
在这些条条框框的加持下,你的代码能够清晰的表现出你的思路和意图。这样不管是自己还是别人,后续阅读代码时能够快速准确的理解,并做出合理的修改。
设计模式
24种设计模式大部分人都阅读过,但轮到自己写时难免会抓瞎。这并不奇怪,能在实际工作中正确的使用其中几种,就已经算佼佼者了。比起全部知道但一个都不会用,这里我推荐掌握以下几种模式:
装饰模式
策略模式
状态模式
代理模式
为什么会推荐这几种?因为在接手一个项目时,我们很容易遇到需要拓展原功能的场景。在你还不能完全了解项目全貌的时候,这几种模式可以使你新业务的代码与旧业务的代码和谐共处。
同时推荐一个专门学习Android场景下设计模式的网站(https://www.kodeco.com/18409174-common-design-patterns-and-app-architectures-for-android#)。作者对每一种模式都做了适用场景的描述,辅以对应的代码和插图,非常的用心。
架构模式
不管是MVC、MVP、MVVM、MVI,都是一种划分角色的体现。有了角色分工,大家才能各司其职,按照协议交流,避免相互干扰。笼统的看,架构模式区划分出三种角色:UI、ViewModel、Repo(sitory)。它们的属性如下:
View负责UI维护
Repo负责数据维护
View和Repo不依赖其他角色
View和Repo可以被复用
大量的业务逻辑堆砌在ViewModel中
ViewModel依赖View和Repo
MVC
MVC应该是最早提出的一种架构模式,它将代码分为这三个角色:
Model:数据维护
View:UI
Controller:处理数据、业务和UI之间的关系。一般由Fragment或Activity承担
MVC是一种比较基础的代码隔离模式。它提出的View和Model角色,在后面的模式进化中都保留了下来,Controller充当它们的Adapter。三者之间没有强制的隔离,也就是说,角色间两两可以直接调用,形成耦合关系。在后续遇到修改时,不可避免的会导致依赖修改。同时,充当Controller角色的Fragment或Activity非常容易膨胀,演变出数千乃至数万行代码。
MVP
在MVC的基础上演化出了MVP架构模式,相较于MVC,它有三个明显的变动:
Activity和Fragment不再代理Controller,专注于处理View。而原先Controller的任务全部交由专门的Presenter处理
使用接口定义View和Model。在接口定义不变的情况下,View和Model内部实现可以自由调整,甚至可以将View和Model替换成接口相同的其他的实现对象,做到了复用
View与Model完全隔离,彼此间不知道对方的存在。所有的通信均通过Presenter代理
MVP真正实现了隔离、复用这些优点。但由于Presenter代理了所有的通信任务,接口会变得非常的庞杂。
MVVM
借助DataBinding等一系列组件,MVVM简化了原先需要手动处理的双向通信,只剩下View → ViewModel(响应UI事件)和ViewModel → Repo(发起Fetch事件)两处通信需要手动处理。Repo和ViewModel的Reaction信息全部通过诸如LiveData
、RxJava
自动反馈在可观察数据中,从而降低MVP中出现的接口复杂度。
MVI
MVI在MVVM的基础上,更加强调了数据与状态间的转换关系,从而弱化了ViewModel层的重要性。简单的说,如果数据流能直接转换对应成UI状态,那么View和Model将可以直接通信,不需要ViewModel层作为中间层中转通信或者存储中间数据。
总结下四种架构模式的递进演变过程:
MVC实现了初级角色定义和代码隔离
MVP在MVP的基础上断开了Model与View的直接交互能力
MVVM在MVP的基础上简化了通信成本
MVI在MVVM的基础上简化了ViewModel的逻辑
效率提升
规范的代码能有效的提升阅读效率,减少低级错误。如何使得自己的代码更加规范?这里有两层含义:
使自己的代码风格贴近整个团队。一个成熟的团队基本上都形成了适合自己团队的代码规范,这些规范包含Tab/4 spaces、中文Encode编码格式、try-catch-finally Return规则等等。可以了解后在Android Studio的设置中对齐
使用Android Studio自带的Lint工具,或者一些第三方lint插件(比如detekt),对方法行数、单行字数、函数复杂度、异常处理方式等作出规则限制,IDE标黄或者直接编译出错
同时,熟悉Android Studio的常用快捷键,比如:
Format Code
Go to Line
Go to Class
Search Everywhere
Run
Rename Class/Variant
Find Usage
Navigate Back/Forward
平时留意一些别人推荐的Android Studio插件,比如:
模版输入Live Template
简单的抓包插件OkHttp Profiler
ADB快捷方式ADB Idea
如果平时有一些日常重复执行的工作,尽早将它们写成脚本,比如更新设计资源、图片压缩、拉取翻译等等。
类似Copilot和Tabnine之类的AI学习插件可以学习你的代码风格,以便作出更加符合你预期的代码提示,甚至直接生成完整的格式代码。在非生产环境下,如果机器性能够用,也推荐多多使用。
Debug
模拟器是Debug时非常好的工具,它可以用于补充真机一些难以覆盖的场景,比如:弱网络、低电量、Mock GPS位置、Mock传感器数据、运行特定版本的系统ROM等。经历了早起几乎不可用的时代,现在Android Studio自带的模拟器已经可以非常流畅的使用了。
Log是调试时几乎不可能绕过的一个点。使用Log定位问题几乎适用于所有的调试场景,但是Log使用时同样有需要注意的点:
Log是有性能开销的,尽量少打不必要的Log,不要在循环和递归中输出Log
Log主要集中在异常场景,用于排查问题时使用。Log单次输出的内容信息尽量完整:入参、错误类型、调用堆栈等
选择合理的日志等级
Debug:只有Debug的时候才需要看,生产环境不会输出
Info:非错误,正常流程的触发
Warning:预期内的异常,不会对使用造成影响或者影响较小
Error:预期外的异常,会对使用造成影响(关键业务不可用、crash等)
不要在Log中输出敏感信息(密码、Token、真实信息等)
断点(Breakpoint)是另一种常用的调试手段,它可以在指定的位置挂起代码的执行,并做以下的事情:
查看成员变量内容
查看调用栈
通过Evaluate Expression临时插入代码执行
断点的使用场景存在一定的约束:
App启动时不适合断点
多线程业务场景不适合断点
主线程不适合断点
调试Api时,可以使用诸如Inspect、Charles、OkHttp Profiler等工具进行抓包,这里不做过多赘述。
一个好的开发同样得是一个好的测试,开发需要对自己的代码负责,而不是寄希望于其他人帮你兜底。这里枚举了一些常用的单元测试框架及适用的场景:
感谢你看到这里。如果本文中大部分的内容你已经掌握,那么恭喜你已经是一位合格的Android开发者了~