文章目錄
前言
本文總結了在項目中做的一次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
vehicle_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_vehicle_recovery_delete.xml
建議使用 FrameLayout 而 不是用 LinearLayout 作爲外層 Parent。
background 應該設置在 ConstraintLayout 上 而不是外層的 Parent。
item_vehicle_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_vehicle.xml
第 8 行,android:orientation="vertical"
無用,建議刪除。
10.重複代碼太多
VehicleDeleteDialog
private fun initList() {
list.clear()
val reasonEntity1 = VehicleReasonEntity()
val reasonEntity2 = CarReasonEntity()
val reasonEntity3 = CarReasonEntity()
reasonEntity1.reasonContent ="原因1"
reasonEntity2.reasonContent = "原因2"
reasonEntity3.reasonContent = "原因3"
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(ResourcesUtil.getString(R.string.vehicele_reason_one))
list.add(VehicleRCartity(ResourcesUtil.getString(R.string.vehicle_reason_two))
list.add(VehicleReasonEntCarourcesUtil.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(ResourcesUtil.getString(R.string.vehicle_reson_one))
list.add(VehicleReasoCar(ResourcesUtil.getString(R.string.vehicle_reason_three list.add(VehicleReasonEntity(ResourcesUtil.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)