文章目录
前言
本文总结了在项目中做的一次CodeReview实例,考虑到安全因素,重要的代码已改名、混淆或删除。
文本重在记录是如何怎样做的。希望对你有所帮助。
CodeReiview 代码范围:以时间线范围为准:2019.5.7-2019.6.8
目的:代码、规范、总结+实例、工具推荐等等。
CodeReiview 工具使用:
执行Android studio –> Analyze –> Inspect code操作之后,所有的lint警告列表就会出来。
于是得到六大类Android Lint
- Correctness 正确性
- Internationalization 国际化,如字符缺少翻译等问题。
- Performance 性能,例如在 onMeasure、onDraw 中执行 new,内存泄露,产生了冗余的资源,xml 结构冗余等。
- Security 安全性,例如没有使用 HTTPS 连接 Gradle,AndroidManifest 中的权限问题等。
- Usability 易用性,例如缺少某些倍数的切图,重复图标等。
- Accessibility 无障碍例如 ImageView 缺少contentDescription 描述,String 编码字符串等问题。
在实际项目中的Code Review情况
开发人员A:
1.性能布局优化
activity_car_index.xml
第245行
<LinearLayout
android:id="@+id/llVehicleRecordCount"
android:layout_width="match_parent"
android:baselineAligned="false"
android:layout_height="49dp"
android:background="@android:color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent">
优化提示:Set ‘android:baselineAligned=“false”’ on this element for better performance
有两点好处:
- 控件对齐:android:baselineAligned(基线对齐)设置为true时同时设置了layout_weight属性控件的对齐方式会根据控件内部的内容对齐,当设置为false时会根据控件的上方对齐。
- 加速绘制:如果LinearLayout被用于嵌套的layout空间计算,它的android:baselineAligned属性应该设置成false,以加速layout计算。
扩展:LinearLayout中的baselineAligned属性
2.消除硬编码、警告等
- 在
shortToast()
;TextView
,距离中使用了硬编码; - 在
activity_car_index.xml
中, 用了ConstraintLayout
后,其他子控件都要相应约束。
如在CarFragment中第181行
tvName.text = "$name · $shopName"
改成用placeholders //String.format(R.string.xx)
如:
-
删除多余的属性声明
<ImageView android:id="@+id/ivBack" android:layout_width="59dp" android:layout_height="43dp" android:layout_gravity="start|center_vertical" android:paddingStart="15dp" android:paddingLeft="15dp" android:paddingTop="14dp" android:paddingEnd="25dp" android:paddingRight="25dp" android:paddingBottom="14dp" android:src="@drawable/ic_common_back" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" />
属性中的right和End、left和Start,建议只保留一个最新的api接口
3.图片压缩
登录启动页面的图片可以压缩后再使用,不影响分辨率,安装包减少(4M左右)。
在线压缩:https://tinypng.com
压缩后图片减少了80%
4.用Space替换View 来进一步减少过度绘制(讨论)
使用场景:
当你需要在2个UI控件添加间距的时候,你可能会添加padding
或margin
。有时最终的layout文件是非常混乱,可读性非常差。
当你需要解决问题时,你突然意识到这里有一个5dp的paddingTop
,那里有一个2dp的marginBottom
,还有一个4dp的paddingBottom
在第三个控件上然后你很难弄明白到底是哪个控件导致的问题。
还有我发现有些人在2个控件之间添加LinearLayou
t或View
来解决这个问题,看起来是一个很简单解决方案但是对App的性能有很大的影响。
原因:
Space is a lightweight View subclass that may be used to create gaps between components in general purpose layouts.
如果你看过Space的源码实现会发现Space继承View但是没有绘制任何东西在canvas。
同时你也会发现在约束布局中:androidx.constraintlayout.widget.Guideline
中的draw中也没有没有执行任何方法。
/**
* Draw nothing.
*
* @param canvas an unused parameter.
*/
@Override
public void draw(Canvas canvas) {
}
Space控件
如果给条目中间添加间距
- 添加view :增加了view 增加了控件 ,影响性能
- 使用layout_marginTop:使用过多的margin 影响代码的可读性
可用轻量级的space
替代
<Space
android:layout_width="match_parent"
android:layout_height="50dp" />
5.移除系统的主题颜色,在各个界面中添加绘制,减少一次过度绘制。
编译警告:
Possible overdraw: Root element paints background ‘@color/bgGray’ with a theme that also paints a background (inferred theme is ‘@style/AppTheme’)
解决方案:尽量把background部分的颜色放子布局中,或者自定义主题,将背景色设置进主题,在运用主题。
在系统主题中(common/styles.xml)修改windowBackground
:
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!--TODO:移除主题背景色,减少一次界面过度绘制-->
<!--<item name="android:windowBackground">@android:color/white</item>-->
<item name="android:windowBackground">@null</item>
<!--或者-->
<!--<item name="android:windowBackground">@android:color/transparent</item>-->
</style>
移除后,系统默认主题会变成黑色,此时需要在各个界面上检测是否没有设置颜色。
在项目中需要优化的地方有:
1.过度绘制(在反馈、关于、修改密码、设置、提交认证等)37个界面。
开发人员B:
6.存在代码注释
git 作为版本管理工具,本身就是来管理不同版本的代码,如果不再使用,就删除代码,如果需要恢复,从 git 记录里也很方面找到。
如:MainActivity 49 - 53 行
7.通用组件冗余
能放在自己组件的东西尽量放在自己的组件,不要往公用组件堆积东西。
CarEntity 新增了一个 isSelected 字段,建议在车辆组件新增一个类,继承自 CarEntity ,将 isSelected 放在该类中,可以参考 CarItemEntity 类。在实际场景中,由于使用了 CarAdapter ,建议直接将 isSelected 放在 CarItemEntity 而不是 CarEntity 中。
小结:通用和组件,可总结成规范,使用继承,类似于使用装饰者模式,添加新的类再继承通过的类。
8.命名
common 组件 dimens.xml 的 dp_0_5 作为 0.5dp 的感觉可能会引起歧义,和 5dp 的命名很像,建议使用 dp_0p5等不一样的命名。
car 组件 styles.xml RightTopPopAnim 没有使用组件前缀。(需求不明确而保留动画,可优化)
car 组件 styles.xml pop_anim 没有使用组件前缀。
CarAdapter isSlideMode 的意义太大了,建议修改成 isShowDeleteButton
小结: 代码规范统一后即可优化,命名规范尽量明确化。
9.布局优化:
-
背景重复设置
-
存在无用布局嵌套
-
子元素 很少的时候,或者就只有一个,建议使用 FrameLayout ,而不是 ConstraintLayout 或 LinearLayout。
优先级:FrameLayout ->LinearLayout->ConstraintLayout ->RelativeLayout
car_activity_recycling_station.xml
-
7 和 21 行设置了重复的背景,考虑到绘制优化,最好只是设置在需要的 view 上,这里是 RecyclerView,不要设置在整个布局上
-
一层 LinearLayout 就够了,不必使用两层
-
完整代码如下:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <cc.xx.common.widget.TitleLayout android:id="@id/titleLayout" android:layout_width="match_parent" android:layout_height="@dimen/titleHeight" app:title_layout_rightText="@string/vehicle_edit" app:title_layout_titleText="@string/vehicle_recycling_station_title" /> <cc.xx.common.widget.layoutstatus.LayoutStatusView android:id="@+id/statusLayout" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1"> <androidx.recyclerview.widget.RecyclerView android:id="@+id/rvList" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@color/common_bg_gray_light" android:paddingStart="@dimen/dp_15" android:paddingTop="@dimen/dp_15" android:paddingEnd="@dimen/dp_15" tools:listitem="@layout/item_vehicle_recycling" /> </cc.xx.common.widget.layoutstatus.LayoutStatusView> <!--底部--> <androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/clBottom" android:layout_width="match_parent" android:layout_height="@dimen/dp_55" android:background="@color/common_bg_white" android:visibility="gone" app:layout_constraintBottom_toBottomOf="parent" tools:visibility="visible"> <ImageView android:id="@+id/ivSelectedAll" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="@dimen/dp_20" android:layout_marginTop="@dimen/dp_16" android:background="@drawable/vehicle_rb_normal" android:contentDescription="@string/app_name" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <cc.xx.lib.widget.CustomTextView android:id="@+id/tvSelectedAll" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="@dimen/dp_8" android:layout_marginTop="@dimen/dp_16" android:text="@string/vehicle_selected_all" android:textColor="@color/common_font_black" android:textSize="@dimen/font_16" app:layout_constraintStart_toEndOf="@id/ivSelectedAll" app:layout_constraintTop_toTopOf="parent" /> <Button android:id="@+id/btnTotalDelete" android:layout_width="@dimen/dp_120" android:layout_height="@dimen/dp_55" android:background="@color/common_bg_orange_red" android:text="@string/vehicle_total_delete" android:textColor="@color/common_font_white" android:visibility="gone" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" tools:visibility="visible" /> <Button android:id="@+id/btnRecovery" android:layout_width="@dimen/dp_120" android:layout_height="@dimen/dp_55" android:background="@color/green07" android:text="@string/vehicle_recovery" android:textColor="@color/common_font_white" app:layout_constraintEnd_toStartOf="@id/btnTotalDelete" app:layout_constraintTop_toTopOf="parent" /> </androidx.constraintlayout.widget.ConstraintLayout> </LinearLayout>
layout_car_recovery_delete.xml
建议使用 FrameLayout 而 不是用 LinearLayout 作为外层 Parent。
background 应该设置在 ConstraintLayout 上 而不是外层的 Parent。
item_car_recycling.xml
不需要外面的LinearLayout,建议移除。
LinearLayout 和 里面的 ConstraintLayout 都设置了背景,而且 ConstraintLayout 的背景把 LinearLayout 背景的圆角效果给遮住了。
最终建议:移除LinearLayout,将 ConstraintLayout 的背景由 @android:color/white 改为 @drawable/common_bg_rect_white_radius_8。
(dialog中为了计算高度可能会用到最外层的LinrearLaytoutk 或FrameLayout,RecyclerView中的item可不用最外层的)
item_car.xml
第 8 行,android:orientation="vertical"
无用,建议删除。
10.重复代码太多
VehicleDeleteDialog
private fun initList() {
list.clear()
val reasonEntity1 = VehicleReasonEntity()
val reasonEntity2 = CarReasonEntity()
val reasonEntity3 = CarReasonEntity()
reasonEntity1.reasonContent =getString(R.string.vehicele_reason_one)
reasonEntity2.reasonContent = getString(R.string.vehicele_reason_two)
reasonEntity3.reasonContent = getString(R.string.vehicele_reason_three)
list.add(reasonEntity1)
list.add(reasonEntity2)
list.add(reasonEntity3)
adapter.setNewData(list)
}
可以修改 CarReasonEntity 代码:
class CarReasonEntity(
// 删除原因
var reasonContent: String = ""
) {
//是否选中
var isSelected = false
}
initList 方法修改为:
private fun initList() {
list.clear()
list.add(CarReasonEntity(getString(R.string.vehicele_reason_one))
list.add(VehicleRCartity(getString(R.string.vehicle_reason_two))
list.add(VehicleReasonEnty(getString(R.string.vehicle_reason_three))
adapter.setNewData(list)
}
或者在将初始化的数据放到 Adapter类中的init中
class HomeAdapter : MyBaseAdapter<CarReasonEntity>(R.layout.item_home) {
//放到这里
init {
list.clear()
list.add(VehiCaronEntity(getString(R.string.vehicle_reson_one))
list.add(VehicleReasoCar(getString(R.string.vehicle_reason_three))
list.add(VehicleReasonEntity(getString(R.string.vehicle_reason_three))
setNewData(list)
}
override fun convert(helper: BaseViewHolder, item: HomeBusinessEntity) {
}
}
11.其他
CarRecyclingAdapter setEditMode 和 selectedAll 调用 notifyDataSetChanged 应该放在 if 里,有满足条件再执行。
bg_common_rect_gray_radius_4.xml 和 bg_common_rect_red_radius_4.xml 放在了 drawable-xhdpi,建议放在 drawable。(图片放的位置不能粗心)
CarRecyclingStationActivity 第 79 行 判断建议增加一个变量来判断,而不是比较字符串。(开发习惯统一)
CarDeleteDialog 第 222 和 223 行,如果确认为必须代码,建议将其移动到基础库 InputUtil.hideSoftInput 方法中。(焦点获取处理)
12.不明确
1.注解放在了方法上,对于具体的注解,放在具体的语句上是否更合适?
- AccountActivity 71行
- LoginActivity 46 行
(消除警告,这个需求不明确,可重新优化)
2.ImageView 的 contentDescription 使用 app_name 是否合适?
(这个也是为了消除警告,Google的人性化设计,可统一优化成“默认为空或其他”)
3.将部分对列表UI的操作放到适配器中,这样减少的Activity中的代码,是否需要提取到规范
- CarRecyclingAdapter 的方法 setEditMode 和 selectedAll
(可提取到规范)
4.考虑App崩溃恢复后,保留之前用户数据录入数据的情况
- CarDeleteDialog、CarRecyclingStationActivity
ConfirmCarInfoActivity 参考 不保留历史活动
(产品需求明确或怎么体验更好)
5.类中方法顺序:个人觉得,采用总、分的方式比较方便。
- CarRecyclingStationActivity 55 行 方法 initRecyclerView 建议 放在方法 loadData之后
(可优化,可总结成代码规范)
6.字符串资源文件占位符,不需要写数字 + $ 符号。
总结
-
明确代码的职责范围,比如说设置背景的位置,命名,范围刚好满足功能就好。
-
缺失一个较为详细的命名规范:布局文件,资源名等
-
缺失组件和通用资源和代码的较为明确的界限划分
CodeReview后,发现开发人员比较好的实现方式和思路,可以总结成开发规范,最好能加上实例,这样方便记录和开发人员的规范统一,做到代码的实现看上去是一个人写的。
以这次为例子:在之前的Android开发规范之上,可以持续完善到开发规范文档中。
开发规范总结完善及应用示例总结(持续完善中):
1.替换Serializable使用Parcelable
2.布局优化总结
渲染原理:渲染大概分为"layout",“measure”"draw"这三个阶段
使用原则:减少布局层级、减少过度绘制、布局复用
使用建议:
-
使用合适的布局
三种常见的ViewGroup的绘制速度:
FrameLayout
>LinerLayout
>RelativeLayout
。ConstraintLayout
是一个更高性能的消灭布局层级的神器使用布局优先级:
FrameLayout>ConstraintLayout>LinearLayout>RelativeLayout
,结合效率和需求实现。 -
尽量减少使用
wrap_content
,推荐使用mathch_parent
或固定尺寸配合gravity="center"
因为 在测量过程中,
match_parent
和固定宽高度对应EXACTLY
,而wrap_content
对应AT_MOST
,这两者对比AT_MOST
耗时较多。 -
在需要的地方添加渲染背景,外层不渲染,在内层需要的地方渲染。
-
文本控件,需要考虑文本过长时的省略策略
-
切图至少提供两套,
xhdpi
和xxhdpi
-
消除布局警告,同时删除控件中的无用属性
使用部分示例:
(1)RecycleView中item 一般用ConstraintLayout
或直接使用控件来布局,以业务需求为准。
(2)简单布局一般用FrameLayout
来布局,同时结合include、merge来使用。
布局文件都要有根节点,但android中的布局嵌套过多会造成性能问题,于是在使用include嵌套的时候我们可以使用merge作为根节点,这样可以减少布局嵌套,提高显示速率。
(3)对于只有在某些条件下才展示出来的组件,建议使用viewStub
包裹起来,include 某布局如果其根布局和引入他的父布局一致,建议使用merge包裹起来,如果你担心preview效果问题,这里完全没有必要,可以tools:showIn=""
属性,这样就可以正常展示preview了。
3.内存优化
1.避免创建不必要的对象 不必要的对象应该避免创建。
如果有需要拼接的字符串,那么可以优先考虑使用StringBuffer
或者StringBuilder
来进行拼接,而不是加号连接符,因为使用加号连接符会创建多余的对象,拼接的字符串越长,加号连接符的性能越低。
如字符串拼接使用
用StringBuffer
的效率高于 String直接“+”拼接
//good
//车牌号=省份+号码
tvPlateNumber.text = StringBuilder(sf).append(hphm)
//bad
tvPlateNumber.text = sf + hphm
2.尽可能地少创建临时对象,越少的对象意味着越少的GC操作。
3.onDraw
方法里面不要执行对象的创建
4.尽量使用基本数据类型替代封装数据类型,如int
比Integer
要更加有效。
4.View异常优化
view自定义控件异常销毁保存状态。在程序异常崩溃时,保存界面相关数据,如用户输入的数据,再次恢复时数据还原,增加用户体验。
示例:
@Override
protected Parcelable onSaveInstanceState() {
//异常情况保存重要信息。
//return super.onSaveInstanceState();
final Bundle bundle = new Bundle();
bundle.putInt("selectedPosition",selectedPosition);
bundle.putInt("flingSpeed",mFlingSpeed);
bundle.putInt("orientation",orientation);
return bundle;
}
@Override
protected void onRestoreInstanceState(Parcelable state) {
if (state instanceof Bundle) {
final Bundle bundle = (Bundle) state;
selectedPosition = bundle.getInt("selectedPosition",selectedPosition);
mFlingSpeed = bundle.getInt("flingSpeed",mFlingSpeed);
orientation = bundle.getInt("orientation",orientation);
return;
}
super.onRestoreInstanceState(state);
}
或者在 OnCreate
和onSaveInstanceState
中读取和保存数据
参考资料:
3.Android Studio 工具:Lint 代码扫描工具(含自定义lint)