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
的item layout 以module_grid_item
開頭【推薦】drawable 資源名稱以
小寫單詞+下劃線
的方式命名,根據分辨率不同存放在
不同的drawable 目錄下,如果介意包大小建議只使用一套,系統去進行縮放。采用
規則如下:
模塊名_業務功能描述_控件描述_控件狀態限定詞
如:module_login_btn_pressed
,module_tabs_icon_home_normal
【推薦】anim 資源名稱以
小寫單詞+下劃線
的方式命名,采用以下規則:
模塊名_邏輯名稱_[方向|序號]
Tween 動畫(使用簡單圖像變換的動畫,例如縮放、平移)資源:盡可能以通用的
動畫名稱命名,如module_fade_in
,module_fade_out
,module_push_down_in (動 畫+方向)
。
Frame 動畫(按幀順序播放圖像的動畫)資源:盡可能以模塊+功能命名+序號
。如
module_loading_grey_001
。【推薦】color 資源使用
#AARRGGBB
格式,寫入module_colors.xml 文件中,命名
格式采用以下規則:
模塊名_邏輯名稱_顏色
如:
<color name="module_btn_bg_color">#33b5e5e5</color>
【推薦】dimen 資源以
小寫單詞+下劃線
方式命名,寫入module_dimens.xml 文件中,
采用以下規則:
模塊名_描述信息
如:
<dimen name="module_horizontal_line_height">1dp</dimen>
【推薦】style 資源采用
父style 名稱.當前style 名稱
方式命名,寫入
module_styles.xml 文件中,首字母大寫。如:
<style name="ParentTheme.ThisActivityTheme"> … </style>
【推薦】string資源文件或者文本用到字符需要全部寫入module_strings.xml 文件中,
字符串以小寫單詞+下劃線
的方式命名,采用以下規則:
模塊名_邏輯名稱
如:moudule_login_tips,module_homepage_notice_desc
【推薦】Id 資源原則上以
駝峰法命名
,View 組件的資源id 建議以View 的縮寫
作為
前綴。常用縮寫表如下:
控件 | 縮寫 |
---|---|
LinearLayout | ll |
RelativeLayout | rl |
ConstraintLayout | cl |
ListView | lv |
ScollView | sv |
TextView | tv |
Button | btn |
ImageView | iv |
CheckBox | cb |
RadioButton | rb |
EditText | et |
其它控件的縮寫推薦使用小寫字母并用下劃線進行分割,例如:ProgressBar 對應
的縮寫為progress_bar;DatePicker 對應的縮寫為date_picker。
10.【推薦】圖片根據其分辨率,放在不同屏幕密度的drawable 目錄下管理,否則可能
在低密度設備上導致內存占用增加,又可能在高密度設備上導致圖片顯示不夠清晰。
說明:
為了支持多種屏幕尺寸和密度,Android 提供了多種通用屏幕密度來適配。常用的
如下。
ldpi - 120dpi
mdpi - 160dpi
hdpi - 240dpi
xhdpi - 320dpi
xxhdpi - 480dpi
xxxhdpi - 640dpi
Android 的屏幕分辨率和密度并不存在嚴格的對應關系,應盡量避免直接基于分辨
率來開發,而是通過適配不同的屏幕密度來保證控件和圖片的顯示效果。不同密度
drawable 目錄中的圖片分辨率設置,參考不同密度的dpi 比例關系。
正例:
為顯示某個圖標,將48 x 48 的圖標文件放在drawable-mdpi 目錄(160dpi)下;
將72 x 72 的圖標文件放在drawable-hdpi 目錄(240dpi)下;將96 x 96 的圖標
文件放在drawable-xhdpi 目錄(320dpi)下;將144 x 144 的圖標文件放在
drawable-xxhdpi 目錄(480dpi)下。
反例:
上述圖標,只有一個144 x 144 的圖標文件放在drawable 目錄下。
Android 基本組件
Android 基本組件指Activity
、Fragment
、Service
、BroadcastReceiver
、
ContentProvider
等等。
【強制】Activity 間的數據通信,對于數據量比較大的,避免使用
Intent + Parcelable
的方式,可以考慮EventBus
等替代方案,以免造成TransactionTooLargeException
。【推薦】
Activity#onSaveInstanceState()
方法不是Activity 生命周期方法,也不保證
一定會被調用。它是用來在Activity 被意外銷毀時保存UI 狀態的,只能用于保存臨
時性數據,例如UI 控件的屬性等,不能跟數據的持久化存儲混為一談。持久化存儲
應該在Activity#onPause()/onStop()
中實行。【強制】Activity 間通過隱式Intent 的跳轉,在發出Intent 之前必須通過
resolveActivity
檢查,避免找不到合適的調用組件,造成ActivityNotFoundException
的異常。
正例:
public void viewUrl(String action, String url, String mimeType) {
Intent intent = new Intent(!TextUtils.isEmpty(action) ? action : Intent.ACTION_VIEW);
if (!TextUtils.isEmpty(url) && !TextUtils.isEmpty(mimeType)) {
intent.setDataAndType(Uri.parse(url), mimeType);
} else if (!TextUtils.isEmpty(url)) {
intent.setData(Uri.parse(url));
} else if (!TextUtils.isEmpty(mimeType)) {
intent.setType(mimeType);
}
if (getPackageManager().resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) != null) {
startActivity(intent);
} else {
// 找不到指定的 Activity
Toast.makeText(this, "找不到指定的Activity", Toast.LENGTH_SHORT).show();
}
}
反例:
Intent intent = new Intent();
intent.setAction("com.example.DemoIntent ");
try {
startActivity(intent);
} catch (ActivityNotFoundException e) {
e.printStackTrace();
}
- 【強制】避免在
Service#onStartCommand()/onBind()
方法中執行耗時操作,如果確
實有需求,應改用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) {
}
}
}
}
- 【強制】避免在
BroadcastReceiver#onReceive()
中執行耗時操作,如果有耗時工作,
應該創建IntentService 完成,而不應該在BroadcastReceiver 內創建子線程去做。
說明:
由于該方法是在主線程執行,如果執行耗時操作會導致UI 不流暢。可以使用
IntentService
、創建HandlerThread
或者調用Context#registerReceiver (BroadcastReceiver, IntentFilter, String, Handler)
方法等方式,在其他Wroker 線程
執行onReceive
方法。BroadcastReceiver#onReceive()
方法耗時超過10 秒鐘,可
能會被系統殺死。
正例:
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, UserHomeService.class);
this.startService(userHomeIntent);
}
};
反例
mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
MyDatabaseHelper myDB = new MyDatabaseHelper(context);
myDB.initData();
// have more database operation here
}
};
- 【強制】避免使用隱式Intent 廣播敏感信息,信息可能被其他注冊了對應
BroadcastReceiver 的App 接收。
說明:
通過Context#sendBroadcast()
發送的隱式廣播會被所有感興趣的receiver 接收,惡
意應用注冊監聽該廣播的receiver 可能會獲取到Intent 中傳遞的敏感信息,并進行
其他危險操作。如果發送的廣播為使用Context#sendOrderedBroadcast()
方法發送
的有序廣播,優先級較高的惡意receiver 可能直接丟棄該廣播,造成服務不可用,
或者向廣播結果塞入惡意數據。
如果廣播僅限于應用內,則可以使用LocalBroadcastManager#sendBroadcast()
實
現,避免敏感信息外泄和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);
以上廣播可能被其他應用的如下receiver 接收導致敏感信息泄漏
final 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()
內調用。
不要隨意使用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 丟失不會造成影響時才可這么做。
正例:
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 class MainActivity extends FragmentActivity {
FragmentManager fragmentManager;
@Override
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();
}
}
【推薦】不要在
Activity#onDestroy()
內執行釋放資源的工作,例如一些工作線程的
銷毀和停止,因為onDestroy()執行的時機可能較晚。可根據實際需要,在
Activity#onPause()/onStop()
中結合isFinishing()
的判斷來執行。【推薦】如非必須,避免使用嵌套的Fragment。
說明:
嵌套Fragment 是在Android API 17添加到SDK以及Support 庫中的功能,Fragment
嵌套使用會有一些坑,容易出現bug,比較常見的問題有如下幾種:
- onActivityResult()方法的處理錯亂,內嵌的Fragment 可能收不到該方法的回調,
需要由宿主Fragment 進行轉發處理; - 突變動畫效果;
- 被繼承的setRetainInstance(),導致在Fragment 重建時多次觸發不必要的邏
輯。
非必須的場景盡可能避免使用嵌套Fragment,如需使用請注意上述問題。
正例:
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();
- 【推薦】總是使用顯式Intent 啟動或者綁定Service,且不要為服務聲明Intent Filter,
保證應用的安全性。如果確實需要使用隱式調用,則可為Service 提供Intent Filter
并從Intent 中排除相應的組件名稱,但必須搭配使用Intent#setPackage()
方法設置
Intent 的指定包名,這樣可以充分消除目標服務的不確定性。
11.【推薦】Service 需要以多線程來并發處理多個啟動請求,建議使用IntentService,
可避免各種復雜的設置。
說明:
Service 組件一般運行主線程,應當避免耗時操作,如果有耗時操作應該在Worker
線程執行。可以使用IntentService 執行后臺任務。
正例:
public class SingleIntentService extends IntentService {
public SingleIntentService() {
super("single-service thread");
}
@Override
protected void onHandleIntent(Intent intent) {
try {
......
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
反例:
public class HelloService extends Service {
...
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Toast.makeText(this, "service starting", Toast.LENGTH_SHORT).show();
new Thread(new Runnable() {
@Override
public void run() {
//操作語句
}
}).start();
...
}
}
12.【推薦】對于只用于應用內的廣播,優先使用LocalBroadcastManager
來進行注冊
和發送,LocalBroadcastManager 安全性更好,同時擁有更高的運行效率。
說明:
對于使用Context#sendBroadcast()
等方法發送全局廣播的代碼進行提示。如果該廣
播僅用于應用內,則可以使用LocalBroadcastManager
來避免廣播泄漏以及廣播被
攔截等安全問題,同時相對全局廣播本地廣播的更高效。
正例:
public class MainActivity extends ActionBarActivity {
private MyReceiver receiver;
private IntentFilter filter;
private Context context;
private static final String MY_BROADCAST_TAG = "com.example.localbroadcast";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstsanceState);
context = this;
setContentView(R.layout.activity_main);
receiver = new MyReceiver();
filter = new IntentFilter();
filter.addAction(MY_BROADCAST_TAG);
Button button = (Button) findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Intent intent = new Intent();
intent.setAction(MY_BROADCAST_TAG);
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
}
});
}
@Override
protected void onResume() {
super.onResume();
LocalBroadcastManager.getInstance(context).registerReceiver(receiver, filter);
}
@Override
protected void onPause() {
super.onPause();
LocalBroadcastManager.getInstance(context).unregisterReceiver(receiver);
}
class MyReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context arg0, Intent arg1) {
// message received
}
}
}
反例:
所有廣播都使用全局廣播
//In activity, sending broadcast
Intent intent = new Intent("com.example.broadcastreceiver.SOME_ACTION");
sendBroadcast(intent);
【推薦】當前Activity 的onPause 方法執行結束后才會創建(onCreate)或恢復
(onRestart)別的Activity,所以在onPause 方法中不適合做耗時較長的工作,這
會影響到頁面之間的跳轉效率。【強制】Activity 或者Fragment 中動態注冊BroadCastReceiver 時,
registerReceiver()
和unregisterReceiver()
要成對出現。
說明:
如果registerReceiver()
和unregisterReceiver()
不成對出現,則可能導致已經注冊的
receiver 沒有在合適的時機注銷,導致內存泄漏,占用內存空間,加重SystemService
負擔。
部分華為的機型會對receiver 進行資源管控,單個應用注冊過多receiver 會觸發管
控模塊拋出異常,應用直接崩潰。
正例:
public class MainActivity extends AppCompatActivity {
private static MyReceiver myReceiver = new MyReceiver();
...
@Override
protected void onResume() {
super.onResume();
IntentFilter filter = new IntentFilter("com.example.myservice");
registerReceiver(myReceiver, filter);
}
@Override
protected void onPause() {
super.onPause();
unregisterReceiver(myReceiver);
}
...
}
反例:
public class MainActivity extends AppCompatActivity {
private static MyReceiver myReceiver;
@Override
protected void onResume() {
super.onResume();
myReceiver = new MyReceiver();
IntentFilter filter = new IntentFilter("com.example.myservice");
registerReceiver(myReceiver, filter);
}
@Override
protected void onDestroy() {
super.onDestroy();
unregisterReceiver(myReceiver);
}
}
Activity 的生命周期不對應,可能出現多次onResume 造成receiver 注冊多個,但
最終只注銷一個,其余receiver 產生內存泄漏。
15.【強制】Android 基礎組件如果使用隱式調用,應在 AndroidManifest.xml 中使用
<intent-filter> 或在代碼中使用 IntentFilter 增加過濾。
說明:
如果瀏覽器支持Intent Scheme Uri 語法,如果過濾不當,那么惡意用戶可能通過瀏
覽器js 代碼進行一些惡意行為,比如盜取cookie 等。如果使用了Intent.parseUri
函數,獲取的intent 必須嚴格過濾。
正例:
// 將intent scheme URL 轉換為intent 對象
Intent intent = Intent.parseUri(uri);
// 禁止沒有BROWSABLE category 的情況下啟動activity
intent.addCategory("android.intent.category.BROWSABLE");
intent.setComponent(null);
intent.setSelector(null);
// 使用intent 啟動activity
context.startActivityIfNeeded(intent, -1)
反例:
Intent intent = Intent.parseUri(uri.toString().trim().substring(15), 0);
intent.addCategory("android.intent.category.BROWSABLE");
context.startActivity(intent);
UI 與布局
【強制】布局中不得不使用ViewGroup 多重嵌套時,不要使用LinearLayout 嵌套,
改用RelativeLayout,可以有效降低嵌套數。
說明:
Android 應用頁面上任何一個View 都需要經過 measure、layout、draw 三個步驟
才能被正確的渲染。從xml layout 的頂部節點開始進行measure,每個子節點都需
要向自己的父節點提供自己的尺寸來決定展示的位置,在此過程中可能還會重新
measure(由此可能導致measure 的時間消耗為原來的2-3 倍)。節點所處位置越深,
嵌套帶來的measure 越多,計算就會越費時。這就是為什么扁平的View 結構會性
能更好。
同時,頁面擁上的View 越多,measure、layout、draw 所花費的時間就越久。要縮
短這個時間,關鍵是保持View 的樹形結構盡量扁平,而且要移除所有不需要渲染的
View。理想情況下,總共的measure,layout,draw 時間應該被很好的控制在16ms
以內,以保證滑動屏幕時UI 的流暢。
要找到那些多余的View(增加渲染延遲的view),可以用Android Studio Monitor
里的Hierarchy Viewer 工具,可視化的查看所有的view。【推薦】在Activity 中顯示對話框或彈出浮層時,盡量使用DialogFragment,而非
Dialog/AlertDialog,這樣便于隨Activity生命周期管理對話框/彈出浮層的生命周期。
正例:
public void showPromptDialog(String text) {
DialogFragment promptDialog = new DialogFragment() {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
getDialog().requestWindowFeature(Window.FEATURE_NO_TITLE);
View view = inflater.inflate(R.layout.fragment_prompt, container);
return view;
}
};
promptDialog.show(getFragmentManager(), text);
}
【推薦】源文件統一采用
UTF-8
的形式進行編碼。【強制】禁止在非UI 線程進行View 相關操作。
【推薦】文本大小使用單位
dp
,View 大小使用單位dp。對于TextView,如果在文
字大小確定的情況下推薦使用wrap_content 布局避免出現文字顯示不全的適配問
題。
說明:
之所以文本大小也推薦使用dp 而非sp,因為sp 是Android 早期推薦使用的,但其
實sp 不僅和dp 一樣受屏幕密度的影響,還受到系統設置里字體大小的影響,所以
使用dp 對于應用開發會更加保證UI 的一致性和還原度。【強制】禁止在設計布局時多次為子View 和父View 設置同樣背景進而造成頁面過
度繪制,推薦將不需要顯示的布局進行及時隱藏。
正例:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/hello" />
<Button
android:id="@+id/btn_mybuttom"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="click it !" />
<ImageView
android:id="@+id/img"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
android:src="@drawable/youtube" />
<TextView
android:text="it is an example!"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</LinearLayout>
反例:
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
int width = getWidth();
int height = getHeight();
mPaint.setColor(Color.GRAY);
canvas.drawRect(0, 0, width, height, mPaint);
mPaint.setColor(Color.CYAN);
canvas.drawRect(0, height/4, width, height, mPaint);
mPaint.setColor(Color.DKGRAY);
canvas.drawRect(0, height/3, width, height, mPaint);
mPaint.setColor(Color.LTGRAY);
canvas.drawRect(0, height/2, width, height, mPaint);
}
【推薦】靈活使用布局,推薦
merge
、ViewStub
來優化布局,盡可能多的減少UI
布局層級,推薦使用FrameLayout
,LinearLayout
、RelativeLayout
次之。【推薦】在需要時刻刷新某一區域的組件時,建議通過以下方式避免引發全局layout
刷新:
- 設置固定的View 大小的寬高,如倒計時組件等;
- 調用View 的layout 方法修改位置,如彈幕組件等;
- 通過修改Canvas 位置并且調用invalidate(int l, int t, int r, int b)等方式限定刷新
區域; - 通過設置一個是否允許requestLayout 的變量,然后重寫控件的requestlayout、
onSizeChanged 方法, 判斷控件的大小沒有改變的情況下, 當進入
requestLayout 的時候,直接返回而不調用super 的requestLayout 方法。
- 【推薦】不能在Activity 沒有完全顯示時顯示PopupWindow 和Dialog。
說明:
Android Activity 創建時的生命周期,按照onCreate() -> onStart() -> onResume() -> onAttachedToWindow() -> onWindowFocusChanged()
的順序, 其中在
Activity#onAttachedToWindow() 時,Activity 會與它的 Window 關聯,這時 UI 才
會開始繪制,在 Activity#onWindowFocusChanged() 時,UI 才變成可交互狀態,
可以提示用戶使用。如果在 Window 未關聯時就創建對話框,UI 可能顯示異常。
推薦的做法是在 Activity#onAttachedToWindow() 之后( 其實最好是
Activity#onWindowFocusChanged() 之后)才創建對話框。
10.【推薦】盡量不要使用AnimationDrawable
,它在初始化的時候就將所有圖片加載
到內存中,特別占內存,并且還不能釋放,釋放之后下次進入再次加載時會報錯。
說明:
Android 的幀動畫可以使用AnimationDrawable 實現,但是如果你的幀動畫中如果
包含過多幀圖片,一次性加載所有幀圖片所導致的內存消耗會使低端機發生OOM
異常。幀動畫所使用的圖片要注意降低內存消耗,當圖片比較大時,容易出現OOM。
正例:
圖片數量較少的AnimationDrawable 還是可以接受的。
<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot ="true">
<item android:duration="500" android:drawable="@drawable/ic_heart_100"/>
<item android:duration="500" android:drawable="@drawable/ic_heart_75"/>
<item android:duration="500" android:drawable="@drawable/ic_heart_50"/>
<item android:duration="500" android:drawable="@drawable/ic_heart_25"/>
<item android:duration="500" android:drawable="@drawable/ic_heart_0"/>
</animation-list>
反例:
<animation-list xmlns:android="http://schemas.android.com/apk/res/android"
android:oneshot ="false">
<item android:drawable="@drawable/soundwave_new_1_40" android:duration="100" />
<item android:drawable="@drawable/soundwave_new_1_41" android:duration="100" />
<item android:drawable="@drawable/soundwave_new_1_42" android:duration="100" />
<item android:drawable="@drawable/soundwave_new_1_43" android:duration="100" />
<item android:drawable="@drawable/soundwave_new_1_44" android:duration="100" />
<item android:drawable="@drawable/soundwave_new_1_45" android:duration="100" />
<item android:drawable="@drawable/soundwave_new_1_46" android:duration="100" />
<item android:drawable="@drawable/soundwave_new_1_47" android:duration="100" />
<item android:drawable="@drawable/soundwave_new_1_48" android:duration="100" />
<item android:drawable="@drawable/soundwave_new_1_49" android:duration="100" />
<item android:drawable="@drawable/soundwave_new_1_50" android:duration="100" />
<item android:drawable="@drawable/soundwave_new_1_51" android:duration="100" />
<item android:drawable="@drawable/soundwave_new_1_52" android:duration="100" />
<item android:drawable="@drawable/soundwave_new_1_53" android:duration="100" />
<item android:drawable="@drawable/soundwave_new_1_54" android:duration="100" />
<item android:drawable="@drawable/soundwave_new_1_55" android:duration="100" />
<item android:drawable="@drawable/soundwave_new_1_56" android:duration="100" />
<item android:drawable="@drawable/soundwave_new_1_57" android:duration="100" />
<item android:drawable="@drawable/soundwave_new_1_58" android:duration="100" />
<item android:drawable="@drawable/soundwave_new_1_59" android:duration="100" />
<item android:drawable="@drawable/soundwave_new_1_60" android:duration="100" />
<item android:drawable="@drawable/soundwave_new_1_61" android:duration="100" />
<item android:drawable="@drawable/soundwave_new_1_62" android:duration="100" />
<item android:drawable="@drawable/soundwave_new_1_63" android:duration="100" />
<item android:drawable="@drawable/soundwave_new_1_64" android:duration="100" />
<item android:drawable="@drawable/soundwave_new_1_65" android:duration="100" />
<item android:drawable="@drawable/soundwave_new_1_66" android:duration="100" />
<item android:drawable="@drawable/soundwave_new_1_67" android:duration="100" />
<item android:drawable="@drawable/soundwave_new_1_68" android:duration="100" />
<item android:drawable="@drawable/soundwave_new_1_69" android:duration="100" />
</animation-list>
上述如此多圖片的動畫就不建議使用AnimationDrawable 了。
11.【強制】不能使用ScrollView
包裹ListView/GridView/ExpandableListVIew
;因為這
樣會把ListView 的所有Item 都加載到內存中,要消耗巨大的內存和cpu 去繪制圖
面。
說明:
ScrollView 中嵌套List 或RecyclerView 的做法官方明確禁止。除了開發過程中遇到
的各種視覺和交互問題,這種做法對性能也有較大損耗。ListView 等UI 組件自身有
垂直滾動功能,也沒有必要在嵌套一層ScrollView。目前為了較好的UI 體驗,更貼
近Material Design 的設計,推薦使用NestedScrollView
。
正例:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout>
<android.support.v4.widget.NestedScrollView>
<LinearLayout>
<ImageView/>
...
<android.support.v7.widget.RecyclerView/>
</LinearLayout>
</android.support.v4.widget.NestedScrollView>
</LinearLayout>
反例:
<ScrollView>
<LinearLayout>
<TextView/>
...
<ListView/>
<TextView />
</LinearLayout>
</ScrollView>
12.【強制】不要在Android 的Application 對象中緩存數據。基礎組件之間的數據共享
請使用Intent 等機制,也可使用SharedPreferences 等數據持久化機制。
反例:
class MyApplication extends Application {
String username;
String getUsername() {
return username;
}
void setUsername(String username) {
this.username = username;
}
}
class SetUsernameActivity extends Activity {
void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.set_username);
MyApplication app = (MyApplication) getApplication();
app.setUsername("tester1");
startActivity(new Intent(this, GetUsernameActivity.class));
}
}
class GetUsernameActivity extends Activity {
TextView tv;
@Override
void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.get_username);
tv = (TextView) findViewById(R.id.username);
}
@Override
void onResume() {
super.onResume();
MyApplication app = (MyApplication) getApplication();
tv.setText("Welcome back ! " + app.getUsername().toUpperCase());
}
}
13.【推薦】使用Toast 時,建議定義一個全局的Toast 對象,這樣可以避免連續顯示
Toast 時不能取消上一次Toast 消息的情況。即使需要連續彈出Toast,也應避免直
接調用Toast#makeText。
14.【強制】使用Adapter 的時候,如果你使用了ViewHolder 做緩存,在getView()的
方法中無論這項convertView 的每個子控件是否需要設置屬性(比如某個TextView
設置的文本可能為null,某個按鈕的背景色為透明,某控件的顏色為透明等),都需
要為其顯式設置屬性(Textview 的文本為空也需要設置setText(""),背景透明也需要
設置),否則在滑動的過程中,因為adapter item 復用的原因,會出現內容的顯示錯
亂。
正例:
@Override
public View getView(int position,View convertView,ViewGroup parent){
ViewHolder myViews;
if(convertView == null){
myViews = new ViewHolder();
convertView = mInflater.inflate(R.layout.list_item,null);
myViews.mUsername = (TextView)convertView.findViewById(R.id.username);
convertView.setTag(myViews);
}else{
myViews = (ViewHolder)convertView.getTag();
}
Info p = infoList.get(position);
String dn = p.getDisplayName;
myViews.mUsername.setText(StringUtils.isEmpty(dn) ? "" : dn);
return convertView;
}
static class ViewHolder {
private TextView mUsername;
}
進程、線程與消息通信
【強制】不要通過Intent 在Android 基礎組件之間傳遞大數據(binder transaction
緩存為1MB),可能導致OOM。【強制】在Application 的業務初始化代碼加入進程判斷,確保只在自己需要的進程
初始化。特別是后臺進程減少不必要的業務初始化。
正例:
public class MyApplication extends Application {
@Override
public void onCreate() {
//在所有進程中初始化
....
//僅在主進程中初始化
if (mainProcess) {
...
}
//僅在后臺進程中初始化
if (bgProcess) {
...
}
}
}
- 【強制】新建線程時,必須通過線程池提供(AsyncTask 或者ThreadPoolExecutor或者其他形式自定義的線程池),不允許在應用中自行顯式創建線程。
說明:
使用線程池的好處是減少在創建和銷毀線程上所花的時間以及系統資源的開銷,解決資源不足的問題。如果不使用線程池,有可能造成系統創建大量同類線程而導致消耗完內存或者“過度切換”的問題。另外創建匿名線程不便于后續的資源使用分析,
對性能分析等會造成困擾。
正例:
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.execute(new Runnnable() {
...
});
反例:
new Thread(new Runnable() {
@Override
public void run() {
//操作語句
...
}
}).start();
- 【強制】線程池不允許使用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();
【強制】子線程中不能更新界面,更新界面必須在主線程中進行,網絡操作不能在
主線程中調用。【推薦】盡量減少不同APP 之間的進程間通信及拉起行為。拉起導致占用系統資源,
影響用戶體驗。【推薦】新建線程時,定義能識別自己業務的線程名稱,便于性能優化和問題排查。
正例:
public class MyThread extends Thread {
public MyThread(){
super.setName("ThreadName");
…
}
}
【推薦】ThreadPoolExecutor 設置線程存活時間(setKeepAliveTime),確保空閑時
線程能被釋放。【推薦】禁止在多進程之間用SharedPreferences 共享數據, 雖然可以
(MODE_MULTI_PROCESS),但官方已不推薦。
10.【推薦】謹慎使用Android 的多進程,多進程雖然能夠降低主進程的內存壓力,但
會遇到如下問題:
- 首次進入新啟動進程的頁面時會有延時的現象(有可能黑屏、白屏幾秒,是白
屏還是黑屏和新Activity 的主題有關); - 應用內多進程時,Application 實例化多次,需要考慮各個模塊是否都需要在所
有進程中初始化。
文件與數據庫
- 【強制】任何時候不要硬編碼文件路徑,請使用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;
}
- 【強制】當使用外部存儲時,必須檢查外部存儲的可用性。
正例:
// 讀/寫檢查
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;
}
- 【強制】應用間共享文件時,不要通過放寬文件系統權限的方式去實現,而應使用
FileProvider。
正例:
<!-- AndroidManifest.xml -->
<manifest>
...
<application>
...
<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.example.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
...
</application>
</manifest>
<!-- res/xml/provider_paths.xml -->
<paths>
<files-path path="album/" name="myimages" />
</paths>
void getAlbumImage(String imagePath) {
File image = new File(imagePath);
Intent getAlbumImageIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
Uri imageUri = FileProvider.getUriForFile(this,"com.example.provider",image);
getAlbumImageIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
startActivityForResult(takePhotoIntent, REQUEST_GET_ALBUMIMAGE);
}
反例:
void getAlbumImage(String imagePath) {
File image = new File(imagePath);
Intent getAlbumImageIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
//不要使用file://的URI 分享文件給別的應用,包括但不限于Intent
getAlbumImageIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(image));
startActivityForResult(takePhotoIntent, REQUEST_GET_ALBUMIMAGE);
}
- 【推薦】SharedPreference 中只能存儲簡單數據類型(int、boolean、String 等),
復雜數據類型建議使用文件、數據庫等其他方式存儲。
正例:
public void updateSettings() {
SharedPreferences mySharedPreferences = getSharedPreferences("settings",Activity.MODE_PRIVATE);
SharedPreferences.Editor editor = mySharedPreferences.edit();
editor.putString("id", "foo");
editor.putString("nick", "bar");
//不要把復雜數據類型轉成String 存儲
editor.apply();
}
- 【推薦】SharedPreference 提交數據時, 盡量使用Editor#apply() , 而非
Editor#commit()。一般來講,僅當需要確定提交結果,并據此有后續操作時,才使
用Editor#commit()。
說明:
SharedPreference 相關修改使用apply 方法進行提交會先寫入內存,然后異步寫入
磁盤, commit 方法是直接寫入磁盤。如果頻繁操作的話apply 的性能會優于commit,
apply 會將最后修改內容寫入磁盤。但是如果希望立刻獲取存儲操作的結果,并據此
做相應的其他操作,應當使用commit。
正例:
public void updateSettingsAsync() {
SharedPreferences mySharedPreferences = getSharedPreferences("settings",
Activity.MODE_PRIVATE);
SharedPreferences.Editor editor = mySharedPreferences.edit();
editor.putString("id", "foo");
editor.apply();
}
public void updateSettings() {
SharedPreferences mySharedPreferences = getSharedPreferences("settings",
Activity.MODE_PRIVATE);
SharedPreferences.Editor editor = mySharedPreferences.edit();
editor.putString("id", "foo");
if (!editor.commit()) {
Log.e(LOG_TAG, "Failed to commit setting changes");
}
}
反例:
editor.putLong("key_name", "long value");
editor.commit();
- 【強制】數據庫Cursor 必須確保使用完后關閉,以免內存泄漏。
說明:
Cursor 是對數據庫查詢結果集管理的一個類,當查詢的結果集較小時,消耗內存不
易察覺。但是當結果集較大,長時間重復操作會導致內存消耗過大,需要開發者在
操作完成后手動關閉Cursor。
數據庫Cursor 在創建及使用時,可能發生各種異常,無論程序是否正常結束,必須
在最后確保Cursor 正確關閉,以避免內存泄漏。同時,如果Cursor 的使用還牽涉
多線程場景,那么需要自行保證操作同步。
正例:
public void handlePhotos(SQLiteDatabase db, String userId) {
Cursor cursor;
try {
cursor = db.query(TUserPhoto, new String[]{"userId", "content"}, "userId=?", new
String[]{userId}, null, null, null);
while (cursor.moveToNext()) {
// TODO
}
} catch (Exception e) {
// TODO
} finally {
if (cursor != null) {
cursor.close();
}
}
}
反例:
public void handlePhotos(SQLiteDatabase db, String userId) {
Cursor cursor = db.query(TUserPhoto, new String[] { "userId", "content" }, "userId=?", new String[] { userId }, null, null, null);
while (cursor.moveToNext()) {
// TODO
}
// 不能放任cursor 不關閉
}
- 【強制】多線程操作寫入數據庫時,需要使用事務,以免出現同步問題。
說明:
通過SQLiteOpenHelper 獲取數據庫SQLiteDatabase 實例,Helper 中會自動緩存已經打開的SQLiteDatabase 實例,單個App 中應使用SQLiteOpenHelper 的單例模式確保數據庫連接唯一。由于SQLite 自身是數據庫級鎖,單個數據庫操作是保證線程安全的(不能同時寫入),transaction 是一次原子操作,因此處于事務中的操作是線程安全的。
若同時打開多個數據庫連接,并通過多線程寫入數據庫,會導致數據庫異常,提示數據庫已被鎖住。
正例:
public void insertUserPhoto(SQLiteDatabase db, String userId, String content) {
ContentValues cv = new ContentValues();
cv.put("userId", userId);
cv.put("content", content);
db.beginTransaction();
try {
db.insert(TUserPhoto, null, cv);
// 其他操作
db.setTransactionSuccessful();
} catch (Exception e) {
// TODO
} finally {
db.endTransaction();
}
}
反例:
public void insertUserPhoto(SQLiteDatabase db, String userId, String content) {
ContentValues cv = new ContentValues();
cv.put("userId", userId);
cv.put("content", content);
db.insert(TUserPhoto, null, cv);
}
- 【推薦】大數據寫入數據庫時,請使用事務或其他能夠提高I/O 效率的機制,保證執
行速度。
正例:
public void insertBulk(SQLiteDatabase db, ArrayList<UserInfo> users) {
db.beginTransaction();
try {
for (int i = 0; i < users.size; i++) {
ContentValues cv = new ContentValues();
cv.put("userId", users[i].userId);
cv.put("content", users[i].content);
db.insert(TUserPhoto, null, cv);
}
// 其他操作
db.setTransactionSuccessful();
} catch (Exception e) {
// TODO
} finally {
db.endTransaction();
}
}
- 【強制】執行SQL 語句時,應使用SQLiteDatabase#insert()、update()、delete(),
不要使用SQLiteDatabase#execSQL(),以免SQL 注入風險。
正例:
public int updateUserPhoto(SQLiteDatabase db, String userId, String content) {
ContentValues cv = new ContentValues();
cv.put("content", content);
String[] args = {String.valueOf(userId)};
return db.update(TUserPhoto, cv, "userId=?", args);
}
反例:
public void updateUserPhoto(SQLiteDatabase db, String userId, String content) {
String sqlStmt = String.format("UPDATE %s SET content=%s WHERE userId=%s",TUserPhoto, userId, content);
//請提高安全意識,不要直接執行字符串作為SQL 語句
db.execSQL(sqlStmt);
}
10.【強制】如果ContentProvider 管理的數據存儲在SQL 數據庫中,應該避免將不受
信任的外部數據直接拼接在原始SQL 語句中。
正例:
// 使用一個可替換參數
String mSelectionClause = "var = ?";
String[] selectionArgs = {""};
selectionArgs[0] = mUserInput;
反例:
// 拼接用戶輸入內容和列名
String mSelectionClause = "var = " + mUserInput;
Bitmap、Drawable 與動畫
- 【強制】加載大圖片或者一次性加載多張圖片,應該在異步線程中進行。圖片的加
載,涉及到IO 操作,以及CPU 密集操作,很可能引起卡頓。
正例:
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
...
// 在后臺進行圖片解碼
@Override
protected Bitmap doInBackground(Integer... params) {
final Bitmap bitmap = BitmapFactory.decodeFile("some path");
return bitmap;
}
...
}
反例:
Button btnLoadImage = (Button) findViewById(R.id.btn);
btnLoadImage.setOnClickListener(new OnClickListener(){
public void onClick(View v) {
Bitmap bitmap = BitmapFactory.decodeFile("some path");
}
});
- 【強制】在ListView,ViewPager,RecyclerView,GirdView 等組件中使用圖片時,
應做好圖片的緩存,避免始終持有圖片導致內存溢出,也避免重復創建圖片,引起
性能問題。建議使用Fresco ( https://github.com/facebook/fresco )、Glide
(https://github.com/bumptech/glide)等圖片庫。
正例:
例如使用系統LruCache 緩存,參考:
https://developer.android.com/topic/performance/graphics/cache-bitmap.html
private LruCache<String, Bitmap> mMemoryCache;
@Override
protected void onCreate(Bundle savedInstanceState) {
...
// 獲取可用內存的最大值,使用內存超出這個值將拋出OutOfMemory 異常。LruCache 通
過構造函數傳入緩存值,以KB 為單位。
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// 把最大可用內存的1/8 作為緩存空間
final int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
return bitmap.getByteCount() / 1024;
}
};
...
}
public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
if (getBitmapFromMemCache(key) == null) {
mMemoryCache.put(key, bitmap);
}
}
public Bitmap getBitmapFromMemCache(String key) {
return mMemoryCache.get(key);
}
public void loadBitmap(int resId, ImageView imageView) {
final String imageKey = String.valueOf(resId);
final Bitmap bitmap = getBitmapFromMemCache(imageKey);
if (bitmap != null) {
mImageView.setImageBitmap(bitmap);
} else {
mImageView.setImageResource(R.drawable.image_placeholder);
BitmapWorkerTask task = new BitmapWorkerTask(mImageView);
task.execute(resId);
}
}
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
...
// 在后臺進行圖片解碼
@Override
protected Bitmap doInBackground(Integer... params) {
final Bitmap bitmap = decodeSampledBitmapFromResource(getResources(),
params[0], 100, 100));
addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
return bitmap;
}
...
}
反例:
沒有存儲,每次都需要解碼,或者有緩存但是沒有合適的淘汰機制,導致緩存效果
很差,依然經常需要重新解碼。
【強制】png 圖片使用TinyPNG 或者類似工具壓縮處理,減少包體積。
【推薦】應根據實際展示需要,壓縮圖片,而不是直接顯示原圖。手機屏幕比較小,
直接顯示原圖,并不會增加視覺上的收益,但是卻會耗費大量寶貴的內存。
正例:
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);
}
反例:
不經壓縮顯示原圖。
- 【強制】使用完畢的圖片,應該及時回收,釋放寶貴的內存。
正例:
Bitmap bitmap = null;
loadBitmapAsync(new OnResult(result){
bitmap = result;
});
...使用該bitmap...
// 使用結束,在2.3.3 及以下需要調用recycle()函數,在2.3.3 以上GC 會自動管理,除非你明
確不需要再用。
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.GINGERBREAD_MR1) {
bitmap.recycle();
}
bitmap = null;
反例:
使用完成圖片,始終不釋放資源。
- 【強制】在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()
}
}
反例:
頁面退出時,不關閉該頁面相關的動畫。
- 【推薦】在動畫或者其他異步任務結束時,應該考慮回調時刻的環境是否還支持業
務處理。例如Activity 的onStop()函數已經執行,且在該函數中主動釋放了資源,
此時回調中如果不做判斷就會空指針崩潰。
正例:
public class MyActivity extends Activity {
private ImageView mImageView;
private Animation mAnimation;
@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);
mAnimation.setAnimationListener(new AnimationListener() {
@Override
public void onAnimationEnd(Animation arg0) {
//判斷一下資源是否被釋放了
if (mImageView != null) {
mImageView.clearAnimation();
}
}
});
mImageView.startAnimation(mAnimation);
}
}
反例:
動畫結束回調中,直接使用資源不加判斷,導致異常。
- 【推薦】使用 inBitmap 重復利用內存空間,避免重復開辟新內存。
正例:
public static Bitmap decodeSampledBitmapFromFile(String filename, int reqWidth, int reqHeight, ImageCache cache) {
final BitmapFactory.Options options = new BitmapFactory.Options();
...
BitmapFactory.decodeFile(filename, options);
...
// 如果在Honeycomb 或更新版本系統中運行,嘗試使用inBitmap
if (Utils.hasHoneycomb()) {
addInBitmapOptions(options, cache);
}
...
return BitmapFactory.decodeFile(filename, options);
}
private static void addInBitmapOptions(BitmapFactory.Options options, ImageCache cache) {
// inBitmap 只處理可變的位圖,所以強制返回可變的位圖
options.inMutable = true;
if (cache != null) {
Bitmap inBitmap = cache.getBitmapFromReusableSet(options);
if (inBitmap != null) {
options.inBitmap = inBitmap;
}
}
}
9.【推薦】使用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);
10【. 推薦】盡量減少 Bitmap(BitmapDrawable)的使用,盡量使用純色(ColorDrawable)、
漸變色(GradientDrawable)、StateSelector(StateListDrawable)等與Shape 結
合的形式構建繪圖。
11.【推薦】謹慎使用gif 圖片,注意限制每個頁面允許同時播放的gif 圖片,以及單個
gif 圖片的大小。
12.【參考】大圖片資源不要直接打包到apk,可以考慮通過文件倉庫遠程下載,減小包
體積。
13.【推薦】根據設備性能,選擇性開啟復雜動畫,以實現一個整體較優的性能和體驗;
14.【推薦】在有強依賴 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);
15.【推薦】當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);
安全
- 【強制】禁止使用常量初始化矢量參數構建IvParameterSpec,建議IV 通過隨機方
式產生。
說明:
使用常量初始化向量,密碼文本的可預測性會高得多,容易受到字典式攻擊。iv 的
作用主要是用于產生密文的第一個block,以使最終生成的密文產生差異(明文相同
的情況下),使密碼攻擊變得更為困難。
正例:
byte[] rand = new byte[16];
SecureRandom r = new SecureRandom();
r.nextBytes(rand);
IvParameterSpec iv = new IvParameterSpec(rand);
反例:
IvParameterSpec iv_ = new IvParameterSpec("1234567890".getBytes());
System.out.println(iv.getIV());
- 【強制】將android:allowbackup 屬性必須設置為false,阻止應用數據被導出。
說明:
android:allowBackup 原本是 Android 提供的 adb 調試功能,如果設置為 true,
可以導出應用數據備份并在任意設備上恢復。這對應用安全性和用戶數據隱私構成
極大威脅,所以必須設置為 false,防止數據泄露。
正例:
<application
android:allowBackup="false"
android:largeHeap="true"
android:icon="@drawable/test_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
- 【強制】如果使用自定義HostnameVerifier 實現類,必須在verify()方法中校驗服務
器主機名的合法性,否則可能受到中間人攻擊。
說明:
在與服務器建立 https 連接時,如果 URL 的主機名和服務器的主機名不匹配,則
可通過該回調接口來判斷是否應該允許建立連接。如果回調內實現不恰當,沒有有
效校驗主機名,甚至默認接受所有主機名,會大大增加安全風險。
反例:
HostnameVerifier hnv = new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
// 不做校驗,接受任意域名服務器
return true;
}
};
HttpsURLConnection.setDefaultHostnameVerifier(hnv);
- 【強制】如果使用自定義X509TrustManager 實現類,必須在checkServerTrusted()
方法中校驗服務端證書的合法性,否則可能受到中間人攻擊。
說明:
常見誤區是checkServerTrusted()方法根本沒有實現,這將導致 X509TrustManager
形同虛設。該方法中需要實現完備的校驗邏輯, 對于證書錯誤拋出
CertificateException 。
正例:
HostnameVerifier hnv = new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
if("yourhostname".equals(hostname)){
return true;
} else {
HostnameVerifier hv = HttpsURLConnection.getDefaultHostnameVerifier();
return hv.verify(hostname, session);
}
}
};
反例:
TrustManager tm = new X509TrustManager() {
public void checkClientTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
//do nothing,接受任意客戶端證書
}
public void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
//do nothing,接受任意服務端證書
}
public X509Certificate[] getAcceptedIssuers() {
return null;
}
};
sslContext.init(null, new TrustManager[] { tm }, null);
【強制】在SDK 支持的情況下,Android 應用必須使用V2 簽名,這將對APK 文件的修改做更多的保護。
【強制】所有的 Android 基本組件(Activity、Service、BroadcastReceiver、ContentProvider 等)都不應在沒有嚴格權限控制的情況下,將 android:exported 設置為 true。
【強制】WebView 應設置 WebView#getSettings()#setAllowFileAccess(false)、
WebView#getSettings()#setAllowFileAccessFromFileURLs(false) 、
WebView#getSettings()#setAllowUniversalAccessFromFileURLs(false),阻止 file
scheme URL 的訪問。
8.【強制】不要把敏感信息打印到log 中。
說明:
在開發過程中,為了方便調試,通常會使用log 函數輸出一些關鍵流程的信息,這
些信息中通常會包含敏感內容,讓攻擊者更加容易了解APP 內部結構,方便破解和
攻擊,甚至直接獲取到有價值的敏感信息。
反例:
String username = "log_leak";
String password = "log_leak_pwd";
Log.d("MY_APP", "usesname" + username);
Log.v("MY_APP", "send message to server ");
以上代碼使用Log.d Log.v 打印程序的執行過程的username 等調試信息,日志沒有
關閉,攻擊者可以直接從Logcat 中讀取這些敏感信息。所以在產品的線上版本中關
閉調試接口,不要輸出敏感信息。
9.【強制】確保應用發布版本的android:debuggable 屬性設置為false。
10.【強制】本地加密秘鑰不能硬編碼在代碼中,更不能使用 SharedPreferences 等本
地持久化機制存儲。應選擇Android 自身的秘鑰庫(KeyStore)機制或者其他安全
性更高的安全解決方案保存。
說明:
應用程序在加解密時,使用硬編碼在程序中的密鑰,攻擊者通過反編譯拿到密鑰可
以輕易解密APP 通信數據。
11.【建議】addJavascriptInterface() 可以添加JS 對本地Java 方法的調用,但這本身
會導致惡意代碼的攻擊。在Android 4.2(API Level 17)以下,不應再使用這樣的
調用方式。在Android 4.2 及以上,需要對本地被遠程調用的方法顯式添加
@JavascriptInterface annotation。
12.【強制】使用Android 的AES/DES/DESede 加密算法時,不要使用ECB 加密模式,
應使用CBC 或CFB 加密模式。
說明:
加密模式有 ECB、CBC、CFB、OFB 等,其中 ECB 的安全性較弱,如果使用固
定的密鑰,相同的明文將會生成相同的密文,容易受到字典攻擊,建議使用 CBC、
CFB 或OFB 等模式。
- ECB:Electronic codebook,電子密碼本模式
- CBC:Cipher-block chaining,密碼分組鏈接模式
- CFB:Cipher feedback,密文反饋模式
- OFB:Output feedback,輸出反饋模式
13.【強制】Android APP 在HTTPS 通信中,驗證策略需要改成嚴格模式。
說明:
Android APP 在HTTPS 通信中,使用ALLOW_ALL_HOSTNAME_VERIFIER,表
示允許和所有的HOST 建立SSL 通信,這會存在中間人攻擊的風險,最終導致敏感
信息可能會被劫持,以及其他形式的攻擊。
反例:
SSLSocketFactory sf = new MySSLSocketFactory(trustStore);
sf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
ALLOW_ALL_HOSTNAME_VERIFIER 關閉host 驗證,允許和所有的host 建立
SSL 通信,BROWSER_COMPATIBLE_HOSTNAME_VERIFIER 和瀏覽器兼容的
驗證策略,即通配符能夠匹配所有子域名 ,STRICT_HOSTNAME_VERIFIER 嚴
格匹配模式,hostname 必須匹配第一個CN 或者任何一個subject-alts,以上例子
使用了ALLOW_ALL_HOSTNAME_VERIFIER,需要改成STRICT_HOSTNAME_
VERIFIER。
14.【推薦】在Android 4.2(API Level 17)及以上,對安全性要求較高的應用可在Activity
中,對 Activity 所關聯的 Window 應用 WindowManager.LayoutParams.FLAG_
SECURE,防止被截屏、錄屏。但要注意的是,一個 Activity 關聯的 Window 可
能不止一個,如果使用了 Dialog / DialogFragment 等控件彈出對話框,它們本身
也會創建一個新的 Window,也一樣需要保護。
15.【推薦】zip 中不要包含 ../../file 這樣的路徑,可能被篡改目錄結構,造成攻擊。
說明:
當zip 壓縮包中允許存在"../"的字符串,攻擊者可以利用多個"../"在解壓時改變zip 文
件存放的位置,當文件已經存在是就會進行覆蓋,如果覆蓋掉的文件是so、dex 或
者odex 文件,就有可能造成嚴重的安全問題。
正例:
對路徑進行判斷,存在".."時拋出異常。
//對重要的Zip 壓縮包文件進行數字簽名校驗,校驗通過才進行解壓
String entryName = entry.getName();
if (entryName.contains("..")){
throw new Exception("unsecurity zipfile!");
}
反例:
BufferedOutputStream dest = null;
try {
ZipInputStream zis = new ZipInputStream(new BufferedInputStream(new FileInputStream
("/Users/yunmogong/Documents/test/test.zip")));
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
int count;
byte data[] = new byte[BUFFER];
String entryName = entry.getName();
FileOutputStream fos = new FileOutputStream(entryName);
//System.out.println("Extracting:" + entry);
dest = new BufferedOutputStream(fos, BUFFER);
while ((count = zis.read(data, 0, BUFFER)) != -1) {
dest.write(data, 0, count);
}
dest.flush();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
dest.close();
} catch (IOException e) {
e.printStackTrace();
}
}
16.【推薦】MD5 和SHA-1、SHA-256 等常用算法是Hash 算法,有一定的安全性,
但不能代替加密算法。敏感信息的存儲和傳輸,需要使用專業的加密機制。
其他
- 【強制】不能使用System.out.println 打印log。
正例:
Log.d(TAG, "Some Android Debug info ...");
反例:
System.out.println("System out println ...");
- 【強制】Log 的tag 不能是" "。
說明:
日志的tag 是空字符串沒有任何意義,也不利于過濾日志。
正例:
private static String TAG = "LoginActivity";
Log.e(TAG, "Login failed!");
反例:
Log.e("", "Login failed!");
參考文獻
[1] Google. Developer Guides [EB/OL].
https://developer.android.com/guide/index.html
[2] Google. Class Index [EB/OL].
https://developer.android.com/reference/classes.html
[3] Alex Lockwood. Android Design Patterns [EB/OL].
https://www.androiddesignpatterns.com/
[4] O'Reilly. High Performance Android Apps by Doug Sillars [EB/OL].
https://www.safaribooksonline.com/library/view/high-performance-android/97814
91913994/ch04.html#figure-story_tree
[5] Takeshi Terada. Whitepaper – Attacking Android browsers via intent scheme
URLs [EB/OL].
https://www.mbsd.jp/Whitepaper/IntentScheme.pdf
[6] 張明云. Android 開發中,有哪些坑需要注意? [EB/OL].
https://zhuanlan.zhihu.com/p/20309921
[7] MegatronKing. Android 多個Fragment 嵌套導致的三大BUG [EB/OL].
http://blog.csdn.net/megatronkings/article/details/51417510
[8] Nfrolov. Android: SQLiteDatabase locking and multi-threading [EB/OL].
https://nfrolov.wordpress.com/2014/08/16/android-sqlitedatabase-locking-and-m
ulti-threading
[9] gcoder_io. Android 數據庫模塊搭建方案 [EB/OL].
http://www.lxweimin.com/p/57eb08fe071d
---The end---