最近跟一个朋友聊到关于App架构的问题, 其中就聊到一个App, 开发了很长时间, 一开始没有去想框架的事儿, 迭代过程中, 由于时间紧, 任务重, 人员更替等原因, 也没能保证代码质量, 很多设计原则被抛之脑后, 代码质量逐步下降, 以致难于阅读, 难于维护. 进而导致迭代困难, 而形成恶性循环.
从而引申出如何重构App代码的话题, 谈点个人理解:
代码无法分出层次, 无法分清业务线.
各个业务模块间/层次间的代码互相夹杂.
由于多人协作导致的多种架构(MVP/MVVM/MVC等)并存.
规范性问题, 导致各个模块内的代码形式互相不一致, 风格迥异.
超长函数, 超大类
代码的格式不规范或不一致.
冗余代码, 无用代码, 重复代码.
过于高明, 使用一些不常用的小技巧而且没有相关注释.
滥用继承, 接口实现等, 导致难以跟踪.
维护困难, 前一发动全身.
不具备扩展灵活性, 无法很快引入系统版本更新时新特性.
不具备可变更性, 产品添加新功能或修改需求时需要修改大量的代码.
重构的目的就是要提高代码质量, 而高质量的代码指标个人认为有如下几点, 当然其实也是老生常谈的几点.
排名分先后:
规范一致性.
结构, 层次明了.
命名有含义, 注释要清晰.
逻辑简短, 没有长篇大幅的代码块.
方法提取, 类继承关系合理.
不滥用设计模式.
聪明是可读性的敌人.
杜绝魔鬼数字/字符串/尺寸值/颜色值等
代码复用, 以便维护.
不写死, 预测可能的变化(但不要提前设计).
良好的分层结构, MVx模式运用.
通过一些设计模式的使用来提高可扩展性.
开闭原则: 修改关闭, 扩展开放.
首先让我们重温下"重构"的含义:
<<重构 --- 改善既有代码的设计>> 这本大神作品强烈建议大家翻阅下~ 里面对重构的定义, 以及如何从一个个小的Bad Smell开始重构等都有详细的描述.
那么作为一个进行已久的Android工程, 我们应该如何重构呢?
其实这是一个对症下药的问题, 针对为什么要重构提出的几个代码问题, 重构也可以分成以下几步:
根据App的业务场景(展示型, 交互型, 后台工具型...)选择合适的架构.
并不是说一定要选用一个架构, 比如说后台工具型的App, 可能界面不多, 也服务器的交互也少, 基本是由Service组成, 可能直接用Android原生的结构就可以.
界面较多, 且与服务器交互较多的建议选用MVP架构. 可以通过P来做数据处理, 将数据源M与展示层V解耦, 便于替换数据源或是改变UI.
根据选用的架构以及业务模块分包
ListView/RecyclerView的选择, Fragment/Activity的选择等.
根据业务特点和选择的架构, 选用相关技术/开源库支持或对当前使用的进行整理.
例如HTTP请求库, 缓存库, 图片加载库等等.
制定编码规范, 可以根据Google推荐的Java编码规范, 适当定制.例如我的项目中的基本规范.
制定代码提交规范, git flow管理流程规范等.
从底部开始, 也就是常说的Model层,数据层开始, 因为这部分相对独立, 可以通过提供接口与UI层隔离.
不要一下就大面积重构, 需要逐个小的case进行重构验证, 保证当前运行.
持续进行小的重构, 每次重构需要伴随测试, 保证重构结果.
提取方法, 去除重复代码.
结构调整.
融入面向对象/接口编程思想, 注意SOLID原则.
多用组合, 少用继承
......
最好有单元测试支持.
不到万不得已不要想重写.
重写会产生各种意想不到的问题, 诸如设计过度, 对于当前代码把握不够(例如现在看起来很不友好的代码可能就是为了解决一个架构无法解决的问题等).
写完此文, 偶然机会在InfoQ上看到Uber的技术主管Raffi Krikorian在 O’Reilly Software Architecture conference上谈及的关于架构重构的12条规则, 共勉之:
Re-Architecture 12 Rules.png
以下附属Android开发编程规范
包名全部采用小写
主包名采用[公司性质].[公司名称].[项目名称]的命名方式
如果根据不同情况进行分包的话,可以将包名分别命名为util,view, adapter等。
命名规则有很多高大上的名词,比如大驼峰,小驼峰,匈牙利命名法。其实最简单的就是按照谷歌命名学习。
常量、枚举等均采用大写形式,用下划线区分各单词。使用static final
例如:private static final String TAG_FOR_ACTIVITY = "XXXX";
类名、接口名、枚举名。第一个和后面的单词都要第一个字母大写
例如:MainActivity
,PersonalLoginActivity
资源文件命名
例如:activity_main.xml
,ic_launcher.png
注意图片文件命名只能用小写字母、数字,否则会导致R文件无法编译出来。也是比较费心的。
继承自安卓组件的类,一般采用父类名作为后缀,
例如:class LoginActivity extends Activity{}
自定义异常必须以Exception
结尾
全局变量添加所有者前缀:实例成员变量前缀m(表示member),类静态变量前缀s(表示static),
例如:protected Subscription mSubscription;
控件变量添加组件前缀,顺序在所有者前缀之后,控件缩写button->btn,textview ->txw,listview->lst等
例如:全局名称mBtnNext
局部名称btnNext
构造方法采用递增方式(参数多的写在后面),参数少的调用参数多的构造函数。这样也减少初始化代码。比如开源库PagerSlidingTabStrip
源文件编码格式为 UTF-8。
java代码中不出现中文,最多注释中可以出现中文
服务端可以实现的,就不要放在客户端
引用第三方库要慎重,避免应用大容量的第三方库,导致客户端包非常大
处理应用全局异常和错误,将错误以邮件的形式发送给服务端
图片的.9处理
使用静态变量方式实现界面间共享要慎重
单元测试(逻辑测试、界面测试)
不要重用父类的handler,对应一个类的handler也不应该让其子类用到,否则会导致message.what冲突
activity中在一个View.OnClickListener中处理所有的逻辑
strings.xml中使用%1$s实现字符串的通配
数据一定要效验,例如字符型转数字型,如果转换失败一定要有缺省值;服务端响应数据是否有效判断
对于未完成的方法,使用TODO加以标记
若功能已完成,但存在效率等潜在问题时,使用XXX加以标记
若代码存在严重问题或仅用于调试,使用FIXME加以标记
values目录下文件名称较固定,不得随意更改
我们使用的无论是git,还是svn都需要遵守下面这些规范,个人比较倾向于git。
工作目录要及时更新,不要和服务器有太大的差别
提交代码时,如果出现冲突,必须仔细分析解决,不可以强行提交
提交代码之前先在本地进行测试,确保项目能编译通过,且能够正常运行,不可盲目提交
必须保证服务器上的版本是正确的,项目有错误时,不要进行提交
提交之前先更新
提交时注意不要提交本地自动生成的文件,比如我们Android Studio项目中的 idea
,build
文件夹是不需要提交的。
不要提交自己不明白的代码
提前协调好项目组成员的工作计划,减少冲突
对提交的信息采用明晰的标注(写注释)
使用git以及github,相信stormzhang的从0开始学习 GitHub 系列会对你有很大的帮助。
这是我整个系列文章从零开始搭建android框架系列的重点,所以这里放在最后面。
是选择MVP,MVC,MVVM ,Flux还是clean 架构?
,+dagger2?+rxjava?+Retrofit/okhtttp?+loader?+databinding?+contentProvider?
谷歌官方架构示例android-architecture,以及我之前github中整理的架构合集能给你答案。
对开源库的选取,一般都需要选择比较稳定的版本,还有作者在维护的项目
,比如这里在github搜索image,出现的安卓中的图片加载库。除了考虑star,还要考虑作者对issue的解决,以及开发者的知名度等各方面。
选取之后,一定的封装是必要的。
网络图片加载的封装这篇文章可能会从图片加载封装的角度给你讲讲封装的必要性。
这里尽量写出自己想到的点。
抽象层面上:
提高架构的拓展性是有必要的。
以前的框架可能会出现功能不足的情况,但是因为这点是不可预见的,所以我们选择框架时一定要了解好框架本身的扩展性如何,或者对框架有较深的理解,能够自己扩展框架,
提高架构的稳定性
架构的文档也是必不可少的。
具体操作时:
activity和fragment里面都会有许多重复的操作以及操作步骤,所以我们都需要提供一个BaseActivity和BaseFragment,让所有的activity和fragment都继承这个基类。
来看看我们BaseActivity中都提供了哪些操作:
必要的注释真的会一定程度上的降低你的工作量,而不是提高。
比如说我使用Rxjava做加载数据的操作。这里面的流程可能稍显复杂,但是能够step1, step2的写在上面,能够让别人看懂,自己维护也方便。
数据提供统一的入口。
无论是在mvp,mvc,还是mvvm中,提供一个统一的数据入口,都可以让代码变得更加易于维护。
比如,我使用的DataManager,里面的http还是preference,还是eventpost ,还是database ,都在DataManger里面进行操作,我们只需要与DataManger打交道。
多用组合, 少用继承
提取方法, 去除重复代码。
比如在我的架构中,我会吧imageloader单独的抽取出来作为一个widget,把对RecyclerView的封装单独抽取出来,把下拉刷新上拉加载抽取出来。如下图:
对于必要的工具类抽取也很重要,这在以后的项目中是可以重用的。
不要使用魔鬼数字/字符串/尺寸值/颜色值,正确的命名等
比如日间模式和夜间模式的对应颜色值,一看就很清晰了。
引入Dagger2 减少模块之间的耦合性
Dagger2 是一个依赖注入框架,使用代码自动生成创建依赖关系需要的代码。减少很多模板化的代码,更易于测试,降低耦合,创建可复用可互换的模块。
参考之前的文章 Google官方MVP+Dagger2架构详解
为你的项目引入Rxjava+RxAndroid这些响应式编程吧。极大的减少逻辑代码,让你爱上写代码停不下来。
通过引入 Event Bus(事件总线,这个项目使用的是otto)。它允许我们在Data Layer中发送事件,以便View Layer中的多个组件都能够订阅到这些事件。比如DataManager
中的退出登录方法可以发送一个事件,订阅这个事件的多个Activity在接收到该事件后就能够更改它们的UI视图,从而显示一个登出状态。
当然你也可以有很多的选择,EventBus,Otto,自定义RxBus等。减少回调。
添加日志打印,用于查找错误等。
logger 以及timber是我推荐的。
需要使用BuildConfig.DEBUG标记对Log进行封装,只在调试时输出重要信息,正式版不输出
TODO more
最后,附属 Android开发技术图谱,仅供参考
Android_App_Skill_Map
Java
HTML/JS (Hybrid/Web App)
C/C++ (NDK)
SQL (DB)
Kotlin
Android Studio
Eclipse
Charles
Wireshark
Fiddler
tcpdump
Paw/Postman
monitor
MAT
adb
draw9patch
hierarchyviewer
uiautomatorviewer
Git命令
Github/GitLab
Gerrit
Github pull request
Redmine
JIRA
Bugzilla
Teambition
Tower
Gradle
Jenkins
Travis CI
蒲公英
fir.im
Activity
Service
Content Provider
Broadcast Receiver
Intent/Intent Filter
App Manifest File
Layouts
Widgets
Resources
Animations
设备适配
WiFi
Mobile网络
网络状态监听
Audio/Video
Camera/Gallery
GPS定位
Network定位
百度Map
高德Map
Linux进程
App进程原理
实现方式
原理
判断当前网络类型
使用缓存
启动流程
生命周期回调原理
与View/Window的关系
与Fragment的关系
View/Window关系
View渲染
View事件分发处理流程
编译打包原理
逆向工程分析
热修复
PhoneGap
ionic
React Native
MVC
MVP
MVVM
Flux
Clean Architecture
分包
分层
OOD原则
常用设计模式运用
AOT compilation
GC
Bytecode&.Dex
monkey/monkey runner
UIAutomator
Espresso
Robotium
RxJava
RxAndroid
RxBinding
Android Annotation
ButterKnife
太多
Retrofit
OkHttp
Volley
Glide
Fresco
Picasso
UIL
Dagger2
ORMLite
GreenDAO
Realm
Sugar
Logger
LeakCanary
DbInspector
更多精彩!!!