- 此篇文章是來自Alibaba的原創(chuàng)內(nèi)容。本人只是CV學(xué)習(xí)一下 哈哈哈哈
車同軌,書同文,《阿里巴巴 Android 開發(fā)手冊》既是高效合作的基礎(chǔ),也是深度 創(chuàng)新的開始。 ——淘寶技術(shù)負責人 莊卓然
《阿里巴巴 Android 開發(fā)手冊》是阿里巴巴集團各大 Android 開發(fā)團隊的集體智慧 結(jié)晶和經(jīng)驗總結(jié),將淘寶、天貓、閑魚、釘釘?shù)?App 長期開發(fā)迭代和優(yōu)化經(jīng)驗系統(tǒng)地整 理成冊,以指導(dǎo) Android 開發(fā)者更加高效、高質(zhì)量地進行 App 開發(fā),呈現(xiàn)給用戶體驗好、 性能優(yōu)、穩(wěn)定性佳、安全性高的產(chǎn)品。
《阿里巴巴 Android 開發(fā)手冊》作為阿里巴巴開發(fā)規(guī)約重要的一環(huán),我們的目標是: - 防患未然,提升質(zhì)量意識,降低故障率和維護成本; - 標準統(tǒng)一,提升協(xié)作效率;
- 追求卓越的工匠精神,打磨精品代碼。
本手冊以開發(fā)者為中心視角分為 Java 語言規(guī)范(遵循《阿里巴巴 Java 開發(fā)手冊》), Android 資源文件命名與使用,Android 基本組件,UI 與布局,進程、線程與消息通信, 文件與數(shù)據(jù)庫,Bitmap、Drawable 與動畫,安全,其他等九大部分,根據(jù)約束力強弱, 規(guī)約依次分為強制、推薦、參考三大類: - 【強制】必須遵守,違反本約定或?qū)饑乐氐暮蠊?
- 【推薦】盡量遵守,長期遵守有助于系統(tǒng)穩(wěn)定性和合作效率的提升;
- 【參考】充分理解,技術(shù)意識的引導(dǎo),是個人學(xué)習(xí)、團隊溝通、項目合作的方 向。
對于規(guī)約條目的延伸信息中,“說明”對內(nèi)容做了適當擴展和解釋;“正例”提倡 什么樣的編碼和實現(xiàn)方式;“反例”說明需要提防的雷區(qū),以及錯誤案例。
另外,「阿里巴巴 Android 開發(fā)規(guī)范」認證考試同步上線,詳情請訪問: https://edu.aliyun.com/certification/cldt04
《阿里巴巴 Android 開發(fā)手冊》項目組成員,排名不分先后:蕓墨(淘寶技術(shù)部)、 矢亮(智能場景事業(yè)部)、游僧(淘寶技術(shù)部)、景寶(淘寶技術(shù)部)、鄰云(閑魚技 術(shù)部)、尚節(jié)(貓客技術(shù)部)等,還有很多阿里巴巴移動開發(fā)工程師參與,在此一并表 示感謝,感謝孤盡(《阿里巴巴 Java 開發(fā)手冊》主要作者)對手冊的指導(dǎo)。
一、Java 語言規(guī)范
遵循《阿里巴巴 Java 開發(fā)手冊》 手冊下載地址:https://yq.aliyun.com/articles/69327
阿里巴巴 Android 開發(fā)手冊
二、Android 資源文件命名與使用
- 【推薦】資源文件需帶模塊前綴。
- 【推薦】layout 文件的命名方式。
Activity 的 layout 以 module_activity 開頭
Fragment 的 layout 以 module_fragment 開頭
Dialog 的 layout 以 module_dialog 開頭
include 的 layout 以 module_include 開頭
ListView 的行 layout 以 module_list_item 開頭
RecyclerView 的 item layout 以 module_recycle_item 開頭
GridView 的行 layout 以 module_grid_item 開頭
- 【推薦】 drawable 資源名稱以小寫單詞+下劃線的方式命名,根據(jù)分辨率不同存放 在不同的 drawable 目錄下,建議只使用一套,例如 drawable-xhdpi。采用規(guī)則如下:
模塊名_業(yè)務(wù)功能描述_控件描述_控件狀態(tài)限定詞
如:module_login_btn_pressed,module_tabs_icon_home_normal
- 【推薦】anim 資源名稱以小寫單詞+下劃線的方式命名,采用以下規(guī)則:
模塊名_邏輯名稱_[方向|序號]
tween 動畫資源:盡可能以通用的動畫名稱命名,如 module_fade_in ,
module_fade_out , module_push_down_in (動畫+方向);
frame 動畫資源:盡可能以模 塊+功能命名+序號。如:module_loading_grey_001
- 【推薦】 color 資源使用#AARRGGBB 格式,寫入 module_colors.xml 文件中,命
名格式采用以下規(guī)則:
模塊名_邏輯名稱_顏色
如:
<color name="module_btn_bg_color">#33b5e5e5</color>
6.【推薦】dimen 資源以小寫單詞+下劃線方式命名,寫入 module_dimens.xml 文件中, 采用以下規(guī)則:
模塊名_描述信息 如:
<dimen name="module_horizontal_line_height">1dp</dimen>
- 【推薦】style 資源采用小寫單詞+下劃線方式命名,寫入 module_styles.xml 文件中, 采用以下規(guī)則:
父 style 名稱.當前 style 名稱
如:
<style name="ParentTheme.ThisActivityTheme"> ...
</style>
8.【推薦】string 資源文件或者文本用到字符需要全部寫入 module_strings.xml 文件中, 字符串以小寫單詞+下劃線的方式命名,采用以下規(guī)則:
模塊名_邏輯名稱
如:
moudule_login_tips,module_homepage_notice_desc
-
【推薦】Id 資源原則上以駝峰法命名,View 組件的資源 id 需要以 View 的縮寫作為 前綴。常用縮寫表如下:
image.png
其它控件的縮寫推薦使用小寫字母并用下劃線進行分割,例如: ProgressBar 對應(yīng)的縮寫為 progress_bar
DatePicker 對應(yīng)的縮寫為 date_picker
10.【推薦】大分辨率圖片(單維度超過 1000)大分辨率圖片建議統(tǒng)一放在 xxhdpi 目錄 下管理,否則將導(dǎo)致占用內(nèi)存成倍數(shù)增加。
說明:
為了支持多種屏幕尺寸和密度,Android 為多種屏幕提供不同的資源目錄進行適配。 為不同屏幕密度提供不同的位圖可繪制對象,可用于密度特定資源的配置限定符(在下面詳述) 包括 ldpi(低)、mdpi(中)、 hdpi(高)、xhdpi(超高)、xxhdpi (超 超高)和 xxxhdpi(超超超高)。例如,高密度屏幕的位圖應(yīng)使用 drawable-hdpi/。
正例:
將 144*144 的應(yīng)用圖標 PNG 文件放在 drawable-xxhdpi 目錄
反例:
將 144*144 的應(yīng)用圖標 PNG 文件放在 drawable-mhdpi 目錄
擴展參考:
https://developer.android.com/guide/practices/screens_support.html?hl=zh-cn
為了支持多種屏幕尺寸和密度,Android 為多種屏幕提供不同的資源目錄進行適配。 為不同屏幕密度提供不同的位圖可繪制對象,可用于密度特定資源的配置限定符(在
根據(jù)當前的設(shè)備屏幕尺寸和密度,將會尋找最匹配的資源,如果將高分辨率圖片放
入低密度目錄,將會造成低端機加載過大圖片資源,又可能造成 OOM,同時也是資
源浪費,沒有必要在低端機使用大圖。
三、Android 基本組件
Android 基本組件指 Activity、Fragment、Service、BroadcastReceiver、 ContentProvider 等等。
- 【強制】Activity 間的數(shù)據(jù)通信,對于數(shù)據(jù)量比較大的,避免使用 Intent + Parcelable 的方式,可以考慮 EventBus 等替代方案,以免造成 TransactionTooLargeException。
- 【推薦】Activity#onSaveInstanceState()方法不是 Activity 生命周期方法,也不保證 一定會被調(diào)用。它是用來在 Activity 被意外銷毀時保存 UI 狀態(tài)的,只能用于保存臨 時性數(shù)據(jù),例如 UI 控件的屬性等,不能跟數(shù)據(jù)的持久化存儲混為一談。持久化存儲 應(yīng)該在 Activity#onPause()/onStop()中實行。
3.【強制】Activity 間通過隱式 Intent 的跳轉(zhuǎn),在發(fā)出 Intent 之前必須通過 resolveActivity 檢查,避免找不到合適的調(diào)用組件,造成 ActivityNotFoundException 的異常。
正例:
public void viewUrl(String url, String mimeType) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.parse(url), mimeType);
if (getPackageManager().resolveActivity(intent, PackageManager.MATCH_DEFAULT_ ONLY) != null) {
try {
startActivity(intent);
} catch (ActivityNotFoundException e) {
if (Config.LOGD) {
Log.d(LOGTAG, "activity not found for " + mimeType + " over " + Uri.parse(url).getScheme(), e);
}
}
}
}
反例:
Intent intent = new Intent();
intent.setAction("com.great.activity_intent.Intent_Demo1_Result3");
- 【強制】避免在 Service#onStartCommand()/onBind()方法中執(zhí)行耗時操作,如果確 實有需求,應(yīng)改用 IntentService 或采用其他異步機制完成。
正例:
public class MainActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
}
public void startIntentService(View source) {
Intent intent = new Intent(this, MyIntentService.class);
startService(intent);
}
}
public class MyIntentService extends IntentService {
public MyIntentService() {
super("MyIntentService");
}
@Override
protected void onHandleIntent(Intent intent) {
synchronized (this) {
try {
......
} catch (Exception e) {
}
}
}
}
5.【強制】避免在 BroadcastReceiver#onReceive()中執(zhí)行耗時操作,如果有耗時工作, 應(yīng)該創(chuàng)建 IntentService 完成,而不應(yīng)該在 BroadcastReceiver 內(nèi)創(chuàng)建子線程去做。
說明:
阿里巴巴 Android 開發(fā)手冊
} }
}
由于該方法是在主線程執(zhí)行,如果執(zhí)行耗時操作會導(dǎo)致 UI 不流暢??梢允褂?br>
IntentService 、 創(chuàng) 建 HandlerThread 或 者 調(diào) 用 Context#registerReceiver
(BroadcastReceiver, IntentFilter, String, Handler)方法等方式,在其他 Wroker 線程
執(zhí)行 onReceive 方法。BroadcastReceiver#onReceive()方法耗時超過 10 秒鐘,可
能會被系統(tǒng)殺死。
正例:
IntentFilter filter = new IntentFilter(); filter.addAction(LOGIN_SUCCESS); this.
registerReceiver(mBroadcastReceiver, filter);
mBroadcastReceiver =new BroadcastReceiver() {
@Override
public void onReceive (Context context, Intent intent){
Intent userHomeIntent = new Intent();
userHomeIntent.setClass(this, UseHomeActivity.class);
this.startActivity(userHomeIntent);
}
}
反例:
BroadcastReceiver() {
@Override
public void onReceive (Context context, Intent intent){
MyDatabaseHelper myDB = new MyDatabaseHelper(context);
myDB.initData();
// have more database operation here }
} ;
6.【強制】避免使用隱式 Intent 廣播敏感信息,信息可能被其他注冊了對應(yīng) BroadcastReceiver 的 App 接收。
說明:
onReceive(android.content.Context, android.content.Intent)
通過 Context#sendBroadcast()發(fā)送的隱式廣播會被所有感興趣的 receiver 接收,惡 意應(yīng)用注冊監(jiān)聽該廣播的 receiver 可能會獲取到 Intent 中傳遞的敏感信息,并進行
其他危險操作。如果發(fā)送的廣播為使用 Context#sendOrderedBroadcast()方法發(fā)送 的有序廣播,優(yōu)先級較高的惡意 receiver 可能直接丟棄該廣播,造成服務(wù)不可用,
或者向廣播結(jié)果塞入惡意數(shù)據(jù)。
如果廣播僅限于應(yīng)用內(nèi),則可以使用 LocalBroadcastManager#sendBroadcast()實
現(xiàn),避免敏感信息外泄和 Intent 攔截的風險。
正例:
Intent intent = new Intent("my-sensitive-event");
intent.putExtra("event", "this is a test event");
LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
反例:
Intent intent = new Intent();
v1.setAction("com.sample.action.server_running");
v1.putExtra("local_ip", v0.h);
v1.putExtra("port", v0.i);
v1.putExtra("code", v0.g); v1.putExtra("connected", v0.s);
v1.putExtra("pwd_predefined", v0.r);
if (!TextUtils.isEmpty(v0.t)) {
v1.putExtra("connected_usr", v0.t);
}
context.sendBroadcast(v1);
以上廣播可能被其他應(yīng)用的如下 receiver 接收導(dǎo)致敏感信息泄漏
class MyReceiver extends BroadcastReceiver {
public final void onReceive(Context context, Intent intent) {
if (intent != null && intent.getAction() != null) {
String s = intent.getAction();
if (s.equals("com.sample.action.server_running") {
String ip = intent.getStringExtra("local_ip");
String pwd = intent.getStringExtra("code");
String port = intent.getIntExtra("port", 8888);
boolean status = intent.getBooleanExtra("connected", false);
}
}
}
}
- 【 推 薦 】 添 加 Fragment 時 , 確 保 FragmentTransaction#commit() 在 Activity#onPostResume()或者 FragmentActivity#onResumeFragments()內(nèi)調(diào)用。 不要隨意使用 FragmentTransaction#commitAllowingStateLoss()來代替,任何 commitAllowingStateLoss()的使用必須經(jīng)過 code review,確保無負面影響。
說明:
Activity 可能因為各種原因被銷毀,Android 支持頁面被銷毀前通過
Activity#onSaveInstanceState() 保 存 自 己 的 狀 態(tài) 。 但 如 果 FragmentTransaction.commit()發(fā)生在 Activity 狀態(tài)保存之后,就會導(dǎo)致 Activity 重
建、恢復(fù)狀態(tài)時無法還原頁面狀態(tài),從而可能出錯。為了避免給用戶造成不好的體 驗,系統(tǒng)會拋出 IllegalStateExceptionStateLoss 異常。推薦的做法是在 Activity 的
onPostResume() 或 onResumeFragments() ( 對 FragmentActivity ) 里 執(zhí) 行 FragmentTransaction.commit(),如有必要也可在 onCreate()里執(zhí)行。不要隨意改用
FragmentTransaction.commitAllowingStateLoss()或者直接使用 try-catch 避免
crash,這不是問題的根本解決之道,當且僅當你確認 Activity 重建、恢復(fù)狀態(tài)時,
本次 commit 丟失不會造成影響時才可這么做。
正例:
public class MainActivity extends FragmentActivity {
FragmentManager fragmentManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main2);
fragmentManager = getSupportFragmentManager();
FragmentTransaction ft = fragmentManager.beginTransaction();
MyFragment fragment = new MyFragment();
ft.replace(R.id.fragment_container, fragment);
ft.commit();
}
}
反例:
public void onSaveInstanceState(Bundle outState, PersistableBundle outPersistentState) {
super.onSaveInstanceState(outState, outPersistentState);
fragmentManager = getSupportFragmentManager();
FragmentTransaction ft = fragmentManager.beginTransaction();
MyFragment fragment = new MyFragment();
ft.replace(R.id.fragment_container, fragment);
ft.commit();
}
}
擴展參考:
- https://www.androiddesignpatterns.com/2013/08/fragment-transaction-commit-state-loss.html
- https://developer.android.com/reference/android/app/FragmentTransaction.html#commit(
【推薦】不要在 Activity#onDestroy()內(nèi)執(zhí)行釋放資源的工作,例如一些工作線程的 銷毀和停止,因為 onDestroy()執(zhí)行的時機可能較晚??筛鶕?jù)實際需要,在 Activity#onPause()/onStop()中結(jié)合 isFinishing()的判斷來執(zhí)行。
【推薦】如非必須,避免使用嵌套的 Fragment。
說明:
嵌套 Fragment 是在 Android API 17 添加到 SDK 以及 Support 庫中的功能,
Fragment 嵌套使用會有一些坑,容易出現(xiàn) bug,比較常見的問題有如下幾種:
- onActivityResult()方法的處理錯亂,內(nèi)嵌的 Fragment 可能收不到該方法的回調(diào), 需要由宿主 Fragment 進行轉(zhuǎn)發(fā)處理;
- 突變動畫效果;
3被繼承的setRetainInstance(),導(dǎo)致在 Fragment 重建時多次觸發(fā)不必要的邏
輯
正例:
FragmentManager fragmentManager = getFragmentManager();
Fragment fragment = fragmentManager.findFragmentByTag(FragmentB.TAG); if(null==fragment)
{
FragmentB fragmentB = new FragmentB();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
fragmentTransaction.add(R.id.fragment_container, fragmentB,
FragmentB.TAG).commit();
}
反例
Fragment videoFragment = new VideoPlayerFragment();
FragmentTransaction transaction = currentFragment.getChildFragmentManager().beginTransaction();
transaction.add(R.id.video_fragment, videoFragment).commit()
10【.推薦】總是使用顯式Intent啟動或者綁定Service,且不要為服務(wù)聲明IntentFilter, 保證應(yīng)用的安全性。如果確實需要使用隱式調(diào)用,則可為 Service 提供 Intent Filter 并從 Intent 中排除相應(yīng)的組件名稱,但必須搭配使用 Intent#setPackage()方法設(shè)置 Intent 的指定包名,這樣可以充分消除目標服務(wù)的不確定性。
11.【推薦】Service 需要以多線程來并發(fā)處理多個啟動請求,建議使用 IntentService, 可避免各種復(fù)雜的設(shè)置。
說明:
Service 組件一般運行主線程,應(yīng)當避免耗時操作,如果有耗時操作應(yīng)該在 Worker
線程執(zhí)行。 可以使用 IntentService 執(zhí)行后臺任務(wù)。
12.【推薦】對于只用于應(yīng)用內(nèi)的廣播,優(yōu)先使用 LocalBroadcastManager 來進行注冊 和發(fā)送,LocalBroadcastManager 安全性更好,同時擁有更高的運行效率。
說明:
對于使用 Context#sendBroadcast()等方法發(fā)送全局廣播的代碼進行提示。如果該廣
播僅用于應(yīng)用內(nèi),則可以使用 LocalBroadcastManager 來避免廣播泄漏以及廣播被
攔截等安全問題,同時相對全局廣播本地廣播的更高效。
13【. 推薦】當前 Activity 的 onPause 方法執(zhí)行結(jié)束后才會執(zhí)行下一個 Activity 的 onCreate 方法,所以在 onPause 方法中不適合做耗時較長的工作,這會影響到頁面之間的跳 轉(zhuǎn)效率。
14.【強制】不要在 Android 的 Application 對象中緩存數(shù)據(jù)?;A(chǔ)組件之間的數(shù)據(jù)共享
請使用 Intent 等機制,也可使用 SharedPreferences 等數(shù)據(jù)持久化機制。
15.【推薦】使用 Toast 時,建議定義一個全局的 Toast 對象,這樣可以避免連續(xù)顯示 Toast 時不能取消上一次 Toast 消息的情況(如果你有連續(xù)彈出 Toast 的情況,避免 使用 Toast.makeText)。
16.【強制】使用 Adapter 的時候,如果你使用了 ViewHolder 做緩存,在 getView()的 方法中無論這項 convertView 的每個子控件是否需要設(shè)置屬性(比如某個 TextView 設(shè)置的文本可能為 null,某個按鈕的背景色為透明,某控件的顏色為透明等),都需 要為其顯式設(shè)置屬性(Textview 的文本為空也需要設(shè)置 setText(""),背景透明也需要 設(shè)置),否則在滑動的過程中,因為 adapter item 復(fù)用的原因,會出現(xiàn)內(nèi)容的顯示錯 亂。
17【. 強制】Activity 或者 Fragment 中動態(tài)注冊 BroadCastReceiver 時,registerReceiver() 和 unregisterReceiver()要成對出現(xiàn)。
說明:
如果 registerReceiver()和 unregisterReceiver()不成對出現(xiàn),則可能導(dǎo)致已經(jīng)注冊的
receiver 沒有在合適的時機注銷,導(dǎo)致內(nèi)存泄漏,占用內(nèi)存空間,加重 SystemService
負擔。
部分華為的機型會對 receiver 進行資源管控,單個應(yīng)用注冊過多 receiver 會觸發(fā)管
控模塊拋出異常,應(yīng)用直接崩潰。
四、UI 與布局
【強制】布局中不得不使用 ViewGroup 多重嵌套時,不要使用 LinearLayout 嵌套,
改用 RelativeLayout,可以有效降低嵌套數(shù)。 說明:
Android 應(yīng)用頁面上任何一個 View 都需要經(jīng)過 measure、layout、draw 三個步驟
才能被正確的渲染。從 xml layout 的頂部節(jié)點開始進行 measure,每個子節(jié)點都需 要向自己的父節(jié)點提供自己的尺寸來決定展示的位置,在此過程中可能還會重新
measure(由此可能導(dǎo)致 measure 的時間消耗為原來的 2-3 倍)。節(jié)點所處位置越
深,套嵌帶來的 measure 越多,計算就會越費時。這就是為什么扁平的 View 結(jié)構(gòu)
會性能更好。
同時,頁面擁上的 View 越多,measure、layout、draw 所花費的時間就越久。要縮 短這個時間,關(guān)鍵是保持 View 的樹形結(jié)構(gòu)盡量扁平,而且要移除所有不需要渲染的
View。理想情況下,總共的 measure,layout,draw 時間應(yīng)該被很好的控制在 16ms
以內(nèi),以保證滑動屏幕時 UI 的流暢。
要找到那些多余的 View(增加渲染延遲的 view),可以用 Android Studio Monitor
里的 Hierarachy Viewer 工具,可視化的查看所有的 view。【推薦】在 Activity 中顯示對話框或彈出浮層時,盡量使用 DialogFragment,而非 Dialog/AlertDialog,這樣便于隨 Activity 生命周期管理對話框/彈出浮層的生命周期。
【推薦】源文件統(tǒng)一采用 UTF-8 的形式進行編碼。
【強制】禁止在非 ui 線程進行 view 相關(guān)操作。
【推薦】文本大小使用單位 dp,view 大小使用單位 dp。對于 Textview,如果在文 字大小確定的情況下推薦使用 wrap_content 布局避免出現(xiàn)文字顯示不全的適配問 題。
【強制】禁止在設(shè)計布局時多次設(shè)置子 view 和父 view 中為同樣的背景造成頁面過 度繪制,推薦將不需要顯示的布局進行及時隱藏。
【推薦】靈活使用布局,推薦 Merge、ViewStub 來優(yōu)化布局,盡可能多的減少 UI 布局層級,推薦使用 FrameLayout,LinearLayout、RelativeLayout 次之。
【推薦】在需要時刻刷新某一區(qū)域的組件時,建議通過以下方式避免引發(fā)全局 layout 刷新:
設(shè)置固定的 view 大小的高寬,如倒計時組件等;
調(diào)用 view 的 layout 方式修改位置,如彈幕組件等;
- 【推薦】不能在 Activity 沒有完全顯示時顯示 PopupWindow 和 Dialog
10.【推薦】盡量不要使用 AnimationDrawable,它在初始化的時候就將所有圖片加載
到內(nèi)存中,特別占內(nèi)存,并且還不能釋放,釋放之后下次進入再次加載時會報錯。
說明:
Android 的幀動畫可以使用 AnimationDrawable 實現(xiàn),但是如果你的幀動畫中如果
包含過多幀圖片,一次性加載所有幀圖片所導(dǎo)致的內(nèi)存消耗會使低端機發(fā)生 OOM 異常。幀動畫所使用的圖片要注意降低內(nèi)存消耗,當圖片比較大時,容易出現(xiàn) OOM。
正例:
圖片數(shù)量較少的 AnimationDrawable 還是可以接受的。
11.【強制】不能使用 ScrollView 包裹 ListView/GridView/ExpandableListVIew;因為這 樣會把 ListView 的所有 Item 都加載到內(nèi)存中,要消耗巨大的內(nèi)存和 cpu 去繪制圖 面。
ScrollView 中嵌套 List 或 RecyclerView 的做法官方明確禁止。除了開發(fā)過程中遇到
的各種視覺和交互問題,這種做法對性能也有較大損耗。ListView 等 UI 組件自身有
垂直滾動功能,也沒有必要在嵌套一層 ScrollView。目前為了較好的 UI 體驗,更貼
近 Material Design 的設(shè)計,推薦使用 NestedScrollView。
擴展參考:
- https://developer.android.com/reference/android/widget/ScrollView.html
- https://developer.android.com/reference/android/support/v4/widget/NestedScrollView.html
五、進程、線程與消息通信
【強制】不要通過 Intent 在 Android 基礎(chǔ)組件之間傳遞大數(shù)據(jù)(binder transaction
緩存為 1MB),可能導(dǎo)致 OOM。【強制】在 Application 的業(yè)務(wù)初始化代碼加入進程判斷,確保只在自己需要的進程
初始化。特別是后臺進程減少不必要的業(yè)務(wù)初始化。【強制】新建線程時,必須通過線程池提供(AsyncTask 或者 ThreadPoolExecutor 或者其他形式自定義的線程池),不允許在應(yīng)用中自行顯式創(chuàng)建線程。
說明:
消耗完內(nèi)存或者“過度切換”的問題。另外創(chuàng)建匿名線程不便于后續(xù)的資源使用分析,
對性能分析等會造成困擾。【強制】線程池不允許使用 Executors 去創(chuàng)建,而是通過 ThreadPoolExecutor 的方 式,這樣的處理方式讓寫的同學(xué)更加明確線程池的運行規(guī)則,規(guī)避資源耗盡的風險。
說明:
Executors 返回的線程池對象的弊端如下: 1)
正例:
FixedThreadPool 和 SingleThreadPool : 允 許 的 請 求 隊 列 長 度 為
Integer.MAX_VALUE,可能會堆積大量的請求,從而導(dǎo)致 OOM;
CachedThreadPool 和 ScheduledThreadPool : 允 許 的 創(chuàng) 建 線 程 數(shù) 量 為
Integer.MAX_VALUE,可能會創(chuàng)建大量的線程,從而導(dǎo)致 OOM。
【強制】子線程中不能更新界面,更新界面必須在主線程中進行,網(wǎng)絡(luò)操作不能在 主線程中調(diào)用。
【強制】不要在非 UI 線程中初始化 ViewStub,否則會返回 null。
【推薦】盡量減少不同 APP 之間的進程間通信及拉起行為。拉起導(dǎo)致占用系統(tǒng)資源,
影響用戶體驗。【推薦】新建線程時,定義能識別自己業(yè)務(wù)的線程名稱,便于性能優(yōu)化和問題排查。
【推薦】ThreadPoolExecutor 設(shè)置線程存活時間(setKeepAliveTime),確保空閑時 線程能被釋放。
10.【推薦】禁止在多進程之間用 SharedPreferences 共享數(shù)據(jù),雖然可以 (MODE_MULTI_PROCESS),但官方已不推薦。
11.【推薦】謹慎使用 Android 的多進程,多進程雖然能夠降低主進程的內(nèi)存壓力,但會 遇到如下問題:
不能實現(xiàn)完全退出所有 Activity 的功能;
首次進入新啟動進程的頁面時會有延時的現(xiàn)象(有可能黑屏、白屏幾秒,是白屏還是黑屏和新 Activity 的主題有關(guān));
多進程間通過 SharedPreferences 共享數(shù)據(jù)時不穩(wěn)定。
阿里巴巴 Android 開發(fā)手冊
六、文件與數(shù)據(jù)庫
【強制】任何時候不要硬編碼文件路徑,請使用 Android 文件系統(tǒng) API 訪問。 說明:
Android 應(yīng)用提供內(nèi)部和外部存儲,分別用于存放應(yīng)用自身數(shù)據(jù)以及應(yīng)用產(chǎn)生的用 戶數(shù)據(jù)??梢酝ㄟ^相關(guān) API 接口獲取對應(yīng)的目錄,進行文件操作。【強制】當使用外部存儲時,必須檢查外部存儲的可用性。
【強制】應(yīng)用間共享文件時,不要通過放寬文件系統(tǒng)權(quán)限的方式去實現(xiàn),而應(yīng)使用 FileProvider。
【推薦】SharedPreference 中只能存儲簡單數(shù)據(jù)類型(int、boolean、String 等), 復(fù)雜數(shù)據(jù)類型建議使用文件、數(shù)據(jù)庫等其他方式存儲。
【推薦】SharedPreference 提交數(shù)據(jù)時,盡量使用 Editor#apply(),而非 Editor#commit()。一般來講,僅當需要確定提交結(jié)果,并據(jù)此有后續(xù)操作時,才使 用 Editor#commit()。
說明:
SharedPreference 相關(guān)修改使用 apply 方法進行提交會先寫入內(nèi)存,然后異步寫入 磁盤,commit 方法是直接寫入磁盤。如果頻繁操作的話 apply 的性能會優(yōu)于 commit,【強制】數(shù)據(jù)庫 Cursor 必須確保使用完后關(guān)閉,以免內(nèi)存泄漏。 說明:
Cursor 是對數(shù)據(jù)庫查詢結(jié)果集管理的一個類,當查詢的結(jié)果集較小時,消耗內(nèi)存不易察覺。但是當結(jié)果集較大,長時間重復(fù)操作會導(dǎo)致內(nèi)存消耗過大,需要開發(fā)者在 操作完成后手動關(guān)閉 Cursor。
數(shù)據(jù)庫 Cursor 在創(chuàng)建及使用時,可能發(fā)生各種異常,無論程序是否正常結(jié)束,必須
在最后確保 Cursor 正確關(guān)閉,以避免內(nèi)存泄漏。同時,如果 Cursor 的使用還牽涉
多線程場景,那么需要自行保證操作同步。【強制】多線程操作寫入數(shù)據(jù)庫時,需要使用事務(wù),以免出現(xiàn)同步問題。 說明:
Android 的通過 SQLiteOpenHelper 獲取數(shù)據(jù)庫 SQLiteDatabase 實例,Helper 中會 自動緩存已經(jīng)打開的 SQLiteDatabase 實例,單個 App 中應(yīng)使用 SQLiteOpenHelper
的單例模式確保數(shù)據(jù)庫連接唯一。由于 SQLite 自身是數(shù)據(jù)庫級鎖,單個數(shù)據(jù)庫操作
是保證線程安全的(不能同時寫入),transaction 時一次原子操作,因此處于事務(wù)中
的操作是線程安全的。
若同時打開多個數(shù)據(jù)庫連接,并通過多線程寫入數(shù)據(jù)庫,會導(dǎo)致數(shù)據(jù)庫異常,提示
數(shù)據(jù)庫已被鎖住。【推薦】大數(shù)據(jù)寫入數(shù)據(jù)庫時,請使用事務(wù)或其他能夠提高 I/O 效率的機制,保證執(zhí)
行速度。【強制】執(zhí)行 SQL 語句時,應(yīng)使用 SQLiteDatabase#insert()、update()、delete(), 不要使用 SQLiteDatabase#execSQL(),以免 SQL 注入風險。
10.【強制】如果 ContentProvider 管理的數(shù)據(jù)存儲在 SQL 數(shù)據(jù)庫中,應(yīng)該避免將不受 信任的外部數(shù)據(jù)直接拼接在原始 SQL 語句中,可使用一個用于將 ? 作為可替換參 數(shù)的選擇子句以及一個單獨的選擇參數(shù)數(shù)組,會避免 SQL 注入。
七、Bitmap、Drawable 與動畫
【強制】加載大圖片或者一次性加載多張圖片,應(yīng)該在異步線程中進行。圖片的加
載,涉及到 IO 操作,以及 CPU 密集操作,很可能引起卡頓。【強制】在 ListView,ViewPager,RecyclerView,GirdView 等組件中使用圖片時, 應(yīng)做好圖片的緩存,避免始終持有圖片導(dǎo)致內(nèi)存泄露,也避免重復(fù)創(chuàng)建圖片,引起 性 能 問 題 。 建 議 使 用 Fresco ( https://github.com/facebook/fresco )、 Glide (https://github.com/bumptech/glide)等圖片庫。
例如使用系統(tǒng) LruCache 緩存,參考: https://developer.android.com/topic/performance/graphics/cache-bitmap.html【強制】png 圖片使用 tinypng 或者類似工具壓縮處理,減少包體積。
【推薦】應(yīng)根據(jù)實際展示需要,壓縮圖片,而不是直接顯示原圖。手機屏幕比較小,
直接顯示原圖,并不會增加視覺上的收益,但是卻會耗費大量寶貴的內(nèi)存。
正例:【強制】使用完畢的圖片,應(yīng)該及時回收,釋放寶貴的內(nèi)存。
【推薦】針對不同的屏幕密度,提供對應(yīng)的圖片資源,使內(nèi)存占用和顯示效果達到 合理的平衡。如果為了節(jié)省包體積,可以在不影響 UI 效果的前提下,省略低密度圖 片。
【強制】在 Activity.onPause()或 Activity.onStop()回調(diào)中,關(guān)閉當前 activity 正在執(zhí) 行的的動畫。
【推薦】在動畫或者其他異步任務(wù)結(jié)束時,應(yīng)該考慮回調(diào)時刻的環(huán)境是否還支持業(yè) 務(wù)處理。例如 Activity 的 onStop()函數(shù)已經(jīng)執(zhí)行,且在該函數(shù)中主動釋放了資源, 此時回調(diào)中如果不做判斷就會空指針崩潰。
【推薦】使用 inBitmap 重復(fù)利用內(nèi)存空間,避免重復(fù)開辟新內(nèi)存。
10.【推薦】使用 ARGB_565 代替 ARGB_888,在不怎么降低視覺效果的前提下,減少 內(nèi)存占用
說明:
android.graphics.Bitmap.Config 類中關(guān)于圖片顏色的存儲方式定義:
- ALPHA_8 代表 8 位 Alpha 位圖;
- ARGB_4444 代表 16 位 ARGB 位圖;
- ARGB_8888 代表 32 位 ARGB 位圖;
- RGB_565 代表 8 位 RGB 位圖。
位圖位數(shù)越高,存儲的顏色信息越多,圖像也就越逼真。大多數(shù)場景使用的是
ARGB_8888 和 RGB_565,RGB_565 能夠在保證圖片質(zhì)量的情況下大大減少內(nèi)存
的開銷,是解決 oom 的一種方法。
但是一定要注意 RGB_565 是沒有透明度的,如果圖片本身需要保留透明度,那么
11【.推薦】盡量減少Bitmap(BitmapDrawable)的使用,盡量使用純色(ColorDrawable)、 漸變色(GradientDrawable)、StateSelector(StateListDrawable)等與 Shape 結(jié) 合的形式構(gòu)建繪圖。
12.【推薦】謹慎使用 gif 圖片,注意限制每個頁面允許同時播放的 gif 圖片,以及單個 gif 圖片的大小。
13.【參考】大圖片資源不要直接打包到 apk,可以考慮通過文件倉庫遠程下載,減小包 體積。
14.【推薦】根據(jù)設(shè)備性能,選擇性開啟復(fù)雜動畫,以實現(xiàn)一個整體較優(yōu)的性能和體驗; 15.【推薦】在有強依賴 onAnimationEnd 回調(diào)的交互時,如動畫播放完畢才能操作頁面,onAnimationEnd 可能會因各種異常沒被回調(diào)(參考: https://stackoverflow.com/questions/5474923/onanimationend-is-not-getting-calle d-onanimationstart-works-fine ), 建 議 加 上 超 時 保 護 或 通 過 postDelay 替 代onAnimationEnd。
15.【推薦】在有強依賴 onAnimationEnd 回調(diào)的交互時,如動畫播放完畢才能操作頁
面,onAnimationEnd 可能會因各種異常沒被回調(diào)(參考: https://stackoverflow.com/questions/5474923/onanimationend-is-not-getting-calle d-onanimationstart-works-fine ), 建 議 加 上 超 時 保 護 或 通 過 postDelay 替 代
onAnimationEnd。
16.【推薦】當 View Animation 執(zhí)行結(jié)束時,調(diào)用 View.clearAnimation()釋放相關(guān)資源。
