背景
2018春節餘味尚未消,阿里巴巴為移動開發者們準備了一份遲到的新年禮物——《阿里巴巴Android開發手冊》1.0.1版本。
在此寫下我的閱讀筆記,記錄下自己平時沒有注意的一些問題,規範自己。
正文
1.【強制】Activity 間通過隱式 Intent 的跳轉,在發出 Intent 之前必須通過 resolveActivity 檢查,避免找不到合適的呼叫元件,造成 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) {
startActivity(intent);
} else {
// 找不到指定的 Activity
}
}
複製程式碼
2.【強制】避免在 BroadcastReceiver#onReceive()中執行耗時操作,如果有耗時工作, 應該建立 IntentService 完成,而不應該在 BroadcastReceiver 內建立子執行緒去做。
說明:
由於該方法是在主執行緒執行,如果執行耗時操作會導致 UI 不流暢。可以使用IntentService 、 創 建 HandlerThread 或 者 調 用 Context#registerReceiver (BroadcastReceiver, IntentFilter, String, Handler)方法等方式,在其他 Wroker 執行緒執行 onReceive 方法。BroadcastReceiver#onReceive()方法耗時超過 10 秒鐘,可能會被系統殺死。
3.【 推 薦 】 添 加 Fragment 時 , 確 保 FragmentTransaction#commit() 在 Activity#onPostResume()或者 FragmentActivity#onResumeFragments()內呼叫。 不要隨意使用 FragmentTransaction#commitAllowingStateLoss()來代替,任何 commitAllowingStateLoss()的使用必須經過 code review,確保無負面影響。
說明:
Activity 可能因為各種原因被銷燬,Android 支援頁面被銷燬前通過Activity#onSaveInstanceState() 保 存 自 己 的 狀 態 。 但 如 果FragmentTransaction.commit()發生在 Activity 狀態儲存之後,就會導致 Activity 重 建、恢復狀態時無法還原頁面狀態,從而可能出錯。為了避免給使用者造成不好的體驗,系統會丟擲 IllegalStateExceptionStateLoss 異常。推薦的做法是在 Activity 的onPostResume() 或 onResumeFragments() ( 對 FragmentActivity ) 裡 執 行 FragmentTransaction.commit(),如有必要也可在 onCreate()裡執行。不要隨意改用FragmentTransaction.commitAllowingStateLoss() 或 者 直 接 使 用 try-catch 避 免 crash,這不是問題的根本解決之道,當且僅當你確認 Activity 重建、恢復狀態時,本次 commit 丟失不會造成影響時才可這麼做。
4.【推薦】不要在 Activity#onDestroy()內執行釋放資源的工作,例如一些工作執行緒的 銷燬和停止,因為 onDestroy()執行的時機可能較晚。可根據實際需要,在 Activity#onPause()/onStop()中結合 isFinishing()的判斷來執行。
5.【推薦】總是使用顯式Intent啟動或者繫結Service,且不要為服務宣告IntentFilter, 保證應用的安全性。如果確實需要使用隱式呼叫,則可為 Service 提供 Intent Filter 並從 Intent 中排除相應的元件名稱,但必須搭配使用 Intent#setPackage()方法設定 Intent 的指定包名,這樣可以充分消除目標服務的不確定性。
6.【推薦】對於只用於應用內的廣播,優先使用 LocalBroadcastManager 來進行註冊 和傳送,LocalBroadcastManager 安全性更好,同時擁有更高的執行效率。
說明:
對於使用 Context#sendBroadcast()等方法傳送全域性廣播的程式碼進行提示。如果該廣播僅用於應用內,則可以使用 LocalBroadcastManager 來避免廣播洩漏以及廣播被攔截等安全問題,同時相對全域性廣播本地廣播的更高效。
7.【推薦】當前 Activity 的 onPause 方法執行結束後才會建立(onCreate)或恢復 (onRestart)別的 Activity,所以在 onPause 方法中不適合做耗時較長的工作,這 會影響到頁面之間的跳轉效率。
8.【推薦】文字大小使用單位 dp,View 大小使用單位 dp。對於 TextView,如果在文 字大小確定的情況下推薦使用 wrap_content 佈局避免出現文字顯示不全的適配問 題。
說明:
之所以文字大小也推薦使用 dp 而非 sp,因為 sp 是 Android 早期推薦使用的,但其 實 sp 不僅和 dp 一樣受螢幕密度的影響,還受到系統設定裡字型大小的影響,所以使用 dp 對於應用開發會更加保證 UI 的一致性和還原度。
9.【推薦】使用 Toast 時,建議定義一個全域性的 Toast 物件,這樣可以避免連續顯示 Toast 時不能取消上一次 Toast 訊息的情況。即使需要連續彈出 Toast,也應避免直 接呼叫 Toast#makeText。
10.【強制】執行緒池不允許使用 Executors 去建立,而是通過 ThreadPoolExecutor 的方 式,這樣的處理方式讓寫的同學更加明確執行緒池的執行規則,規避資源耗盡的風險。
說明:
Executors 返回的執行緒池物件的弊端如下:
-
FixedThreadPool 和 SingleThreadPool : 允 許 的 請 求 隊 列 長 度 為Integer.MAX_VALUE,可能會堆積大量的請求,從而導致 OOM;
-
CachedThreadPool 和 ScheduledThreadPool : 允 許 的 創 建 線 程 數 量 為Integer.MAX_VALUE,可能會建立大量的執行緒,從而導致 OOM。
正例:
int NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors();
int KEEP_ALIVE_TIME = 1;
TimeUnit KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS;
BlockingQueue<Runnable> taskQueue = new LinkedBlockingQueue<Runnable>();
ExecutorService executorService = new ThreadPoolExecutor(NUMBER_OF_CORES, NUMBER_OF_CORES*2, KEEP_ALIVE_TIME, KEEP_ALIVE_TIME_UNIT,
taskQueue, new BackgroundThreadFactory(), new DefaultRejectedExecutionHandler());
複製程式碼
反例:
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
複製程式碼
11.【推薦】ThreadPoolExecutor 設定執行緒存活時間(setKeepAliveTime),確保空閒時 執行緒能被釋放.
12. 【推薦】禁止在多程式之間用 SharedPreferences 共享資料,雖然可以 (MODE_MULTI_PROCESS),但官方已不推薦。
13. 【強制】任何時候不要硬編碼檔案路徑,請使用 Android 檔案系統 API 訪問。
說明:
Android 應用提供內部和外部儲存,分別用於存放應用自身資料以及應用產生的用 戶資料。可以通過相關 API 介面獲取對應的目錄,進行檔案操作。
-
android.os.Environment#getExternalStorageDirectory()
-
android.os.Environment#getExternalStoragePublicDirectory()
-
android.content.Context#getFilesDir()
-
android.content.Context#getCacheDir
正例:
public File getDir(String alName) {
File file = new File(Environment.getExternalStoragePublicDirectory(Environment. DIRECTORY_PICTURES), alName);
if (!file.mkdirs()) {
Log.e(LOG_TAG, "Directory not created");
}
return file;
}
複製程式碼
反例:
public File getDir(String alName) {
// 任何時候都不要硬編碼檔案路徑,這不僅存在安全隱患,也讓 app 更容易出現適配問題
File file = new File("/mnt/sdcard/Download/Album", alName);
if (!file.mkdirs()) {
Log.e(LOG_TAG, "Directory not created");
}
return file;
}
複製程式碼
14.【強制】當使用外部儲存時,必須檢查外部儲存的可用性
正例:
// 讀/寫檢查
public boolean isExternalStorageWritable() {
String state = Environment.getExternalStorageState();
if (Environment.MEDIA_MOUNTED.equals(state)) {
return true;
}
return false;
}
// 只讀檢查
public boolean isExternalStorageReadable() {
String state = Environment.getExternalStorageState();
if (Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {
return true;
}
return false;
}
複製程式碼
15.【強制】應用間共享檔案時,不要通過放寬檔案系統許可權的方式去實現,而應使用 FileProvider。
16.【強制】如果 ContentProvider 管理的資料儲存在 SQL 資料庫中,應該避免將不受 信任的外部資料直接拼接在原始 SQL 語句中。
???這是個什麼梗,都沒說清楚???
正例:
// 使用一個可替換引數
String mSelectionClause = "var = ?"; String[] selectionArgs = {""}; selectionArgs[0] = mUserInput;
複製程式碼
反例:
// 拼接使用者輸入內容和列名
String mSelectionClause = "var = " + mUserInput;
複製程式碼
17.【強制】png 圖片使用 TinyPNG 或者類似工具壓縮處理,減少包體積。
18.【推薦】應根據實際展示需要,壓縮圖片,而不是直接顯示原圖。手機螢幕比較小,直接顯示原圖,並不會增加視覺上的收益,但是卻會耗費大量寶貴的記憶體。
正例:
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) {
// 首先通過 inJustDecodeBounds=true 獲得圖片的尺寸
final BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
// 然後根據圖片解析度以及我們實際需要展示的大小,計算壓縮率
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// 設定壓縮率,並解碼
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
複製程式碼
19.【強制】在 Activity#onPause()或 Activity#onStop()回撥中,關閉當前 activity 正在執 行的的動畫。
正例:
public class MyActivity extends Activity {
ImageView mImageView;
Animation mAnimation;
Button mBtn;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
mImageView = (ImageView) findViewById(R.id.ImageView01);
mAnimation = AnimationUtils.loadAnimation(this, R.anim.anim);
mBtn = (Button) findViewById(R.id.Button01);
mBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mImageView.startAnimation(mAnimation);
}
};
}
@Override
public void onPause() {
//頁面退出,及時清理動畫資源
mImageView.clearAnimation();
}
}
複製程式碼
20.【推薦】使用 RGB_565 代替 RGB_888,在不怎麼降低視覺效果的前提下,減少記憶體佔用。
說明:
android.graphics.Bitmap.Config 類中關於圖片顏色的儲存方式定義:
-
ALPHA_8 代表 8 位 Alpha 點陣圖;
-
ARGB_4444 代表 16 位 ARGB 點陣圖;
-
ARGB_8888 代表 32 位 ARGB 點陣圖;
-
RGB_565 代表 8 位 RGB 點陣圖。
點陣圖位數越高,儲存的顏色資訊越多,影象也就越逼真。大多數場景使用的是ARGB_8888 和 RGB_565,RGB_565 能夠在保證圖片質量的情況下大大減少記憶體的開銷,是解決 OOM 的一種方法。
但是一定要注意 RGB_565 是沒有透明度的,如果圖片本身需要保留透明度,那麼就不能使用 RGB_565。
正例:
Config config = drawableSave.getOpacity() != PixelFormat.OPAQUE ? Config.ARGB_8565 : Config.RGB_565;
Bitmap bitmap = Bitmap.createBitmap(w, h, config);
複製程式碼
反例:
Bitmap newb = Bitmap.createBitmap(width, height, Config.ARGB_8888);
複製程式碼
21.【推薦】在有強依賴 onAnimationEnd 回撥的互動時,如動畫播放完畢才能操作頁面,onAnimationEnd 可能會因各種異常沒被回撥(參考: https://stackoverflow.com/questions/5474923/onanimationend-is-not-getting-calle d-onanimationstart-works-fine ), 建 議 加 上 超 時 保 護 或 通 過 postDelay 替 代onAnimationEnd。
正例:
View v = findViewById(R.id.xxxViewID);
final FadeUpAnimation anim = new FadeUpAnimation(v);
anim.setInterpolator(new AccelerateInterpolator());
anim.setDuration(1000);
anim.setFillAfter(true);
new Handler().postDelayed(new Runnable() {
public void run() {
if (v != null) {
v.clearAnimation();
}
}
}, anim.getDuration());
v.startAnimation(anim);
複製程式碼
22.【推薦】當 View Animation 執行結束時,呼叫 View.clearAnimation()釋放相關資源。
正例:
View v = findViewById(R.id.xxxViewID);
final FadeUpAnimation anim = new FadeUpAnimation(v);
anim.setInterpolator(new AccelerateInterpolator());
anim.setDuration(1000);
anim.setFillAfter(true);
anim.setAnimationListener(new AnimationListener() {
@Override
public void onAnimationEnd(Animation arg0) {
//判斷一下資源是否被釋放了
if (v != null) {
v.clearAnimation();
}
});
v.startAnimation(anim);
複製程式碼
總結
說真的,這手冊總結得挺好的,雖然內容少了點,但是才1.0.1版本,還會繼續修改完善的。
我覺得上面的第8點寫得不太合理:
8.【推薦】文字大小使用單位 dp,View 大小使用單位 dp。對於 TextView,如果在文 字大小確定的情況下推薦使用 wrap_content 佈局避免出現文字顯示不全的適配問 題。
說明:
之所以文字大小也推薦使用 dp 而非 sp,因為 sp 是 Android 早期推薦使用的,但其 實 sp 不僅和 dp 一樣受螢幕密度的影響,還受到系統設定裡字型大小的影響,所以使用 dp 對於應用開發會更加保證 UI 的一致性和還原度。
我覺得:如果使用者設定了系統字型大小,那麼肯定是希望系統整體字型變大或變小,而你的APP卻不怎麼變,這看起來一來不協調,二來沒有達到使用者修改系統字型大小的目的,感覺這樣的做法有點破壞系統的生態,不推薦這樣做。