Android組開(kāi)發(fā)規(guī)范
本文參考借鑒阿里Android規(guī)范
一准脂、目標(biāo)
Android組開(kāi)發(fā)規(guī)范用以指導(dǎo)團(tuán)隊(duì)成員疮胖,更高效,高質(zhì)量的對(duì)APP進(jìn)行開(kāi)發(fā),以保證開(kāi)發(fā)出性能優(yōu)異糜颠,穩(wěn)定性佳夸政,安全性高的產(chǎn)品
- 防范于未然匀归,提升質(zhì)量意識(shí)仿便,降低故障率與維護(hù)成本狡赐;
- 統(tǒng)一標(biāo)準(zhǔn)鲤氢,提高協(xié)作效率喷市;
- 匠人精神,打磨精品代碼;
二植酥、Android資源文件命名與使用(統(tǒng)一以小寫單詞加下劃線方式命名)
1.資源文件統(tǒng)一攜帶模塊名(module)前綴(所有前綴通過(guò)小組會(huì)議提前確定卸留,并登記JIRA報(bào)備統(tǒng)計(jì))
2.layout文件的命名方式
- Activity 的 layout 使用 module_activity_類名捻悯,如通用 使用 common_activity_業(yè)務(wù)描述
- Fragment 的 layout 使用 module_fragment_類名,如通用 使用 common_fragment_業(yè)務(wù)描述
- Dialog 的 layout 使用 module_dialog_類名/業(yè)務(wù)描述,如通用 使用 common_dialog_業(yè)務(wù)描述
- include 的 layout 使用 module_include_類名/業(yè)務(wù)描述囱淋,如通用 使用 common_include_業(yè)務(wù)描述
- RecyclerView 的 layout 使用 module_item_類名/業(yè)務(wù)描述,如通用 使用 common_item_業(yè)務(wù)描述
3.drawable資源統(tǒng)一存放在drawable-xxhdpi內(nèi),由系統(tǒng)自動(dòng)縮放
- 非通用情況下,采用 module_業(yè)務(wù)功能描述_狀態(tài)
- 通用情況下,采用 common_業(yè)務(wù)功能描述_狀態(tài)
4.anim資源存放在drawable內(nèi)
- 非通用情況下,采用 module_功能描述_狀態(tài)
- 通用情況下灭必,采用 common_功能描述_狀態(tài)
5.color資源使用具體argb內(nèi)容命名,不區(qū)分模塊
- 無(wú)a部分禁漓,c_000000
- 有a部分跟衅,c_ff000000
- 如果有一套完整統(tǒng)一的主題色,可使用主題色名稱命名播歼,c_module_xx
6.暫不考慮dimens資源
7.style資源采用 ‘父style點(diǎn)當(dāng)前style’命名伶跷,寫入module_styles.xml文件中
- <style name="ParentTheme.currentTheme"></style>
8.string資源文件或者文本用到的字符串全部寫入module_strings.xml文件中
- 采用 module_業(yè)務(wù)描述
9.id資源,以控件View的縮寫為前綴+模塊+功能命名
控件 | 縮寫 |
---|---|
LinearLayout | ll |
RelativeLayout | rl |
ConstraintLayout | cl |
FrameLayout | fl |
ListView | recyclerView代替 |
RecyclerView | rv |
ScrollView | sv |
TextView | tv |
Button | btn |
ImageView | iv |
CheckBox | cb |
RadioButton | rb |
EditText | et |
ProgressBar | pb |
DatePicker | dp |
- 如:ll_module_功能
三颂鸿、基本組件
1.Activity數(shù)據(jù)傳遞基本類型 可使用Intent僚祷,小對(duì)象推薦Pacelable序列化,大對(duì)象可以考慮使用EventBus等方案
2.Activity#onSaveInstanceState()方法不是 Activity 生命周期方法,也不保證
一定會(huì)被調(diào)用。它是用來(lái)在 Activity 被意外銷毀時(shí)保存 UI 狀態(tài)的卿捎,只能用于保存臨
時(shí)性數(shù)據(jù)于个,例如 UI 控件的屬性等汇荐,不能跟數(shù)據(jù)的持久化存儲(chǔ)混為一談安寺。持久化存儲(chǔ)
應(yīng)該在 Activity#onPause()/onStop()中實(shí)行砂代。
3.Activity 間通過(guò)隱式 Intent 的跳轉(zhuǎn),在發(fā)出 Intent 之前必須通過(guò) 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) {
startActivity(intent);
}else {
// 找不到指定的 Activity
}
}
4.避免在 Service#onStartCommand()/onBind()方法中執(zhí)行耗時(shí)操作,如果確
實(shí)有需求,應(yīng)改用 IntentService 或采用其他異步機(jī)制完成上渴。
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í)行耗時(shí)操作,如果有耗時(shí)工作,
應(yīng)該創(chuàng)建 IntentService 完成,而不應(yīng)該在 BroadcastReceiver 內(nèi)創(chuàng)建子線程去做牵舵。
說(shuō)明:
由于該方法是在主線程執(zhí)行柒啤,如果執(zhí)行耗時(shí)操作會(huì)導(dǎo)致 UI 不流暢倦挂。可以使用
IntentService 白修、創(chuàng)建 HandlerThread 或者調(diào)用 Context#registerReceiver
(BroadcastReceiver, IntentFilter, String, Handler)方法等方式妒峦,在其他 Wroker 線程
執(zhí)行 onReceive 方法。BroadcastReceiver#onReceive()方法耗時(shí)超過(guò) 10 秒鐘兵睛,可
能會(huì)被系統(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, UserHomeService.class);
this.startService(userHomeIntent);
}
};
6.避免使用隱式 Intent 廣播敏感信息,信息可能被其他注冊(cè)了對(duì)應(yīng)
BroadcastReceiver 的 App 接收祖很。
說(shuō)明:
通過(guò) Context#sendBroadcast()發(fā)送的隱式廣播會(huì)被所有感興趣的 receiver 接收笛丙,惡
意應(yīng)用注冊(cè)監(jiān)聽(tīng)該廣播的 receiver 可能會(huì)獲取到 Intent 中傳遞的敏感信息,并進(jìn)行
其他危險(xiǎn)操作假颇。如果發(fā)送的廣播為使用 Context#sendOrderedBroadcast()方法發(fā)送
的有序廣播胚鸯,優(yōu)先級(jí)較高的惡意 receiver 可能直接丟棄該廣播,造成服務(wù)不可用笨鸡,
或者向廣播結(jié)果塞入惡意數(shù)據(jù)姜钳。
如果廣播僅限于應(yīng)用內(nèi),則可以使用 LocalBroadcastManager#sendBroadcast()實(shí)
現(xiàn)形耗,避免敏感信息外泄和 Intent 攔截的風(fēng)險(xiǎn)哥桥。
Intent intent = new Intent("my-sensitive-event");
intent.putExtra("event", "this is a test event");
LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
7.添 加 Fragment 時(shí),確保 FragmentTransaction#commit() 在
Activity#onPostResume()或者 FragmentActivity#onResumeFragments()內(nèi)調(diào)用激涤。
不要隨意使用 FragmentTransaction#commitAllowingStateLoss()來(lái)代替拟糕,任何
commitAllowingStateLoss()的使用必須經(jīng)過(guò) code review,確保無(wú)負(fù)面影響倦踢。
說(shuō)明:
Activity 可能因?yàn)楦鞣N原因被銷毀送滞, Android 支持頁(yè) 面被銷毀前通過(guò)
Activity#onSaveInstanceState() 保存自己的狀態(tài)。但如果
FragmentTransaction.commit()發(fā)生在 Activity 狀態(tài)保存之后辱挥,就會(huì)導(dǎo)致 Activity 重
建犁嗅、恢復(fù)狀態(tài)時(shí)無(wú)法還原頁(yè)面狀態(tài),從而可能出錯(cuò)晤碘。為了避免給用戶造成不好的體驗(yàn)愧哟,系統(tǒng)會(huì)拋出 IllegalStateExceptionStateLoss 異常。推薦的做法是在 Activity 的
onPostResume() 或 onResumeFragments() ( 對(duì) FragmentActivity )里執(zhí)行
FragmentTransaction.commit()哼蛆,如有必要也可在 onCreate()里執(zhí)行。不要隨意改用
FragmentTransaction.commitAllowingStateLoss() 或者直接使用 try-catch 避 免
crash霞赫,這不是問(wèn)題的根本解決之道腮介,當(dāng)且僅當(dāng)你確認(rèn) Activity 重建、恢復(fù)狀態(tài)時(shí)端衰,
本次 commit 丟失不會(huì)造成影響時(shí)才可這么做叠洗。
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();
}
}
8.不要在 Activity#onDestroy()內(nèi)執(zhí)行釋放資源的工作甘改,例如一些工作線程的
銷毀和停止,因?yàn)?onDestroy()執(zhí)行的時(shí)機(jī)可能較晚灭抑∈可根據(jù)實(shí)際需要,在
Activity#onPause()/onStop()中結(jié)合 isFinishing()的判斷來(lái)執(zhí)行腾节。
9.如非必須忘嫉,避免使用嵌套的 Fragment。
嵌套Fragment是在Android API 17添加到SDK以及Support庫(kù)中的功能案腺,F(xiàn)ragment
嵌套使用會(huì)有一些坑庆冕,容易出現(xiàn) bug,比較常見(jiàn)的問(wèn)題有如下幾種:
- onActivityResult()方法的處理錯(cuò)亂劈榨,內(nèi)嵌的 Fragment 可能收不到該方法的回調(diào)访递,
需要由宿主 Fragment 進(jìn)行轉(zhuǎn)發(fā)處理;
突變動(dòng)畫效果同辣;
被繼承的 setRetainInstance()拷姿,導(dǎo)致在 Fragment 重建時(shí)多次觸發(fā)不必要的邏
輯。
非必須的場(chǎng)景盡可能避免使用嵌套 Fragment旱函,如需使用請(qǐng)注意上述問(wèn)題响巢。
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();
}
10.總是使用顯式 Intent 啟動(dòng)或者綁定 Service,且不要為服務(wù)聲明 Intent Filter陡舅,
保證應(yīng)用的安全性抵乓。如果確實(shí)需要使用隱式調(diào)用,則可為 Service 提供 Intent Filter
并從 Intent 中排除相應(yīng)的組件名稱靶衍,但必須搭配使用 Intent#setPackage()方法設(shè)置
Intent 的指定包名灾炭,這樣可以充分消除目標(biāo)服務(wù)的不確定性。
11.Service 需要以多線程來(lái)并發(fā)處理多個(gè)啟動(dòng)請(qǐng)求颅眶,建議使用 IntentService蜈出,
可避免各種復(fù)雜的設(shè)置。
說(shuō)明:
Service 組件一般運(yùn)行主線程涛酗,應(yīng)當(dāng)避免耗時(shí)操作铡原,如果有耗時(shí)操作應(yīng)該在 Worker
線程執(zhí)行∩烫荆可以使用 IntentService 執(zhí)行后臺(tái)任務(wù)燕刻。
public class SingleIntentService extends IntentService {
public SingleIntentService() {
super("single-service thread");
}
@Override
protected void onHandleIntent(Intent intent) {
try {
......
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
12.對(duì)于只用于應(yīng)用內(nèi)的廣播,優(yōu)先使用 LocalBroadcastManager 來(lái)進(jìn)行注冊(cè)
和發(fā)送剖笙,LocalBroadcastManager 安全性更好卵洗,同時(shí)擁有更高的運(yùn)行效率。
說(shuō)明:
對(duì)于使用 Context#sendBroadcast()等方法發(fā)送全局廣播的代碼進(jìn)行提示弥咪。如果該廣
播僅用于應(yīng)用內(nèi)过蹂,則可以使用 LocalBroadcastManager 來(lái)避免廣播泄漏以及廣播被
攔截等安全問(wèn)題十绑,同時(shí)相對(duì)全局廣播本地廣播的更高效。
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
}
}
}
13.當(dāng)前 Activity 的 onPause 方法執(zhí)行結(jié)束后才會(huì)創(chuàng)建(onCreate)或恢復(fù)
(onRestart)別的 Activity酷勺,所以在 onPause 方法中不適合做耗時(shí)較長(zhǎng)的工作本橙,這
會(huì)影響到頁(yè)面之間的跳轉(zhuǎn)效率。
14.Activity或者Fragment中動(dòng)態(tài)注冊(cè)BroadCastReceiver時(shí)脆诉,registerReceiver()
和 unregisterReceiver()要成對(duì)出現(xiàn)甚亭。
說(shuō)明:
如果 registerReceiver()和 unregisterReceiver()不成對(duì)出現(xiàn),則可能導(dǎo)致已經(jīng)注冊(cè)的
receiver 沒(méi)有在合適的時(shí)機(jī)注銷库说,導(dǎo)致內(nèi)存泄漏狂鞋,占用內(nèi)存空間,加重 SystemService
負(fù)擔(dān)潜的。
部分華為的機(jī)型會(huì)對(duì) receiver 進(jìn)行資源管控骚揍,單個(gè)應(yīng)用注冊(cè)過(guò)多 receiver 會(huì)觸發(fā)管
控模塊拋出異常,應(yīng)用直接崩潰啰挪。
Activity 的生命周期不對(duì)應(yīng)信不,可能出現(xiàn)多次 onResume 造成 receiver 注冊(cè)多個(gè),但
最終只注銷一個(gè)亡呵,其余 receiver 產(chǎn)生內(nèi)存泄漏抽活。
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);
}
...
}
15.Android 基礎(chǔ)組件如果使用隱式調(diào)用,應(yīng)在 AndroidManifest.xml 中使用
或在代碼中使用 IntentFilter 增加過(guò)濾锰什。
說(shuō)明:
如果瀏覽器支持 Intent Scheme Uri 語(yǔ)法下硕,如果過(guò)濾不當(dāng),那么惡意用戶可能通過(guò)瀏
覽器 js 代碼進(jìn)行一些惡意行為汁胆,比如盜取 cookie 等梭姓。如果使用了 Intent.parseUri
函數(shù),獲取的 intent 必須嚴(yán)格過(guò)濾嫩码。
// 將 intent scheme URL 轉(zhuǎn)換為 intent 對(duì)象
Intent intent = Intent.parseUri(uri);
// 禁止沒(méi)有 BROWSABLE category 的情況下啟動(dòng) activity
intent.addCategory("android.intent.category.BROWSABLE");
intent.setComponent(null);
intent.setSelector(null);
// 使用 intent 啟動(dòng) activity
context.startActivityIfNeeded(intent, -1)
四誉尖、UI與布局
1.布局中不得不使用 ViewGroup 多重嵌套時(shí),不要使用 LinearLayout 嵌套铸题,
改用 RelativeLayout铡恕,最好使用ConstraintLayout約束布局,可以有效降低嵌套數(shù)丢间。
說(shuō)明:
Android 應(yīng)用頁(yè)面上任何一個(gè) View 都需要經(jīng)過(guò) measure探熔、layout、draw 三個(gè)步驟
才能被正確的渲染烘挫。從 xml layout 的頂部節(jié)點(diǎn)開(kāi)始進(jìn)行 measure祭刚,每個(gè)子節(jié)點(diǎn)都需
要向自己的父節(jié)點(diǎn)提供自己的尺寸來(lái)決定展示的位置,在此過(guò)程中可能還會(huì)重新
measure(由此可能導(dǎo)致measure的時(shí)間消耗為原來(lái)的2-3倍)。節(jié)點(diǎn)所處位置越深涡驮,
嵌套帶來(lái)的 measure 越多,計(jì)算就會(huì)越費(fèi)時(shí)喜滨。這就是為什么扁平的 View 結(jié)構(gòu)會(huì)性
能更好捉捅。
同時(shí),頁(yè)面擁上的 View 越多虽风,measure棒口、layout、draw 所花費(fèi)的時(shí)間就越久辜膝。要縮
短這個(gè)時(shí)間无牵,關(guān)鍵是保持 View 的樹形結(jié)構(gòu)盡量扁平,而且要移除所有不需要渲染的
View厂抖。理想情況下茎毁,總共的 measure,layout忱辅,draw 時(shí)間應(yīng)該被很好的控制在 16ms
以內(nèi)七蜘,以保證滑動(dòng)屏幕時(shí) UI 的流暢。
要找到那些多余的 View(增加渲染延遲的 view)墙懂,可以用 Android Studio Monitor
里的 Hierarchy Viewer 工具橡卤,可視化的查看所有的 view。
2.在 Activity 中顯示對(duì)話框或彈出浮層時(shí)损搬,盡量使用 DialogFragment碧库,而非
Dialog/AlertDialog,這樣便于隨Activity生命周期管理對(duì)話框/彈出浮層的生命周期巧勤。
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);
}
3.源文件統(tǒng)一采用 UTF-8 的形式進(jìn)行編碼嵌灰。
4.禁止在非 UI 線程進(jìn)行 View 相關(guān)操作。
5.文本大小使用單位 dp踢关,View 大小使用單位 dp伞鲫。對(duì)于 TextView,如果在文
字大小確定的情況下推薦使用 wrap_content 布局避免出現(xiàn)文字顯示不全的適配問(wèn)
題签舞。
說(shuō)明:
之所以文本大小也推薦使用 dp 而非 sp秕脓,因?yàn)?sp 是 Android 早期推薦使用的,但其
實(shí) sp 不僅和 dp 一樣受屏幕密度的影響儒搭,還受到系統(tǒng)設(shè)置里字體大小的影響吠架,所以
使用 dp 對(duì)于應(yīng)用開(kāi)發(fā)會(huì)更加保證 UI 的一致性和還原度。
6.禁止在設(shè)計(jì)布局時(shí)多次為子 View 和父 View 設(shè)置同樣背景進(jìn)而造成頁(yè)面過(guò)
度繪制搂鲫,推薦將不需要顯示的布局進(jìn)行及時(shí)隱藏傍药。
7.靈活使用布局,推薦 merge、ViewStub 來(lái)優(yōu)化布局拐辽,盡可能多的減少 UI
布局層級(jí)拣挪,推薦使用 FrameLayout,LinearLayout俱诸、RelativeLayout 次之菠劝。
8.在需要時(shí)刻刷新某一區(qū)域的組件時(shí),建議通過(guò)以下方式避免引發(fā)全局 layout
刷新:
- 設(shè)置固定的 View 大小的寬高睁搭,如倒計(jì)時(shí)組件等赶诊;
- 調(diào)用 View 的 layout 方法修改位置,如彈幕組件等园骆;
- 通過(guò)修改 Canvas 位置并且調(diào)用 invalidate(int l, int t, int r, int b)等方式限定刷新
區(qū)域舔痪; - 通過(guò)設(shè)置一個(gè)是否允許requestLayout的變量,然后重寫控件的requestlayout锌唾、
onSizeChanged 方法锄码,判斷控件的大小沒(méi)有改變的情況下,當(dāng)進(jìn)入
requestLayout 的時(shí)候鸠珠,直接返回而不調(diào)用 super 的 requestLayout 方法巍耗。
9.不能在 Activity 沒(méi)有完全顯示時(shí)顯示 PopupWindow 和 Dialog。
說(shuō)明:
Android Activity 創(chuàng)建時(shí)的生命周期渐排,按照 onCreate() -> onStart() -> onResume() ->
onAttachedToWindow() -> onWindowFocusChanged() 的順序炬太,其中在
Activity#onAttachedToWindow() 時(shí),Activity 會(huì)與它的 Window 關(guān)聯(lián)驯耻,這時(shí) UI 才
會(huì)開(kāi)始繪制亲族,在 Activity#onWindowFocusChanged() 時(shí),UI 才變成可交互狀態(tài)可缚,
可以提示用戶使用霎迫。如果在 Window 未關(guān)聯(lián)時(shí)就創(chuàng)建對(duì)話框,UI 可能顯示異常帘靡。
推薦的做法是在 Activity#onAttachedToWindow() 之后(其實(shí)最好是
Activity#onWindowFocusChanged() 之后)才創(chuàng)建對(duì)話框知给。
10.盡量不要使用 AnimationDrawable,它在初始化的時(shí)候就將所有圖片加載
到內(nèi)存中描姚,特別占內(nèi)存涩赢,并且還不能釋放,釋放之后下次進(jìn)入再次加載時(shí)會(huì)報(bào)錯(cuò)轩勘。
說(shuō)明:
Android 的幀動(dòng)畫可以使用 AnimationDrawable 實(shí)現(xiàn)筒扒,但是如果你的幀動(dòng)畫中如果
包含過(guò)多幀圖片,一次性加載所有幀圖片所導(dǎo)致的內(nèi)存消耗會(huì)使低端機(jī)發(fā)生 OOM
異常绊寻。幀動(dòng)畫所使用的圖片要注意降低內(nèi)存消耗花墩,當(dāng)圖片比較大時(shí)悬秉,容易出現(xiàn) OOM。
圖片數(shù)量較少的 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>
11.不能使用 ScrollView 包裹 ListView/GridView/ExpandableListVIew;因?yàn)檫@
樣會(huì)把 ListView 的所有 Item 都加載到內(nèi)存中和泌,要消耗巨大的內(nèi)存和 cpu 去繪制圖
面。
說(shuō)明:
ScrollView 中嵌套 List 或 RecyclerView 的做法官方明確禁止祠肥。除了開(kāi)發(fā)過(guò)程中遇到
的各種視覺(jué)和交互問(wèn)題允跑,這種做法對(duì)性能也有較大損耗。ListView 等 UI 組件自身有
垂直滾動(dòng)功能搪柑,也沒(méi)有必要在嵌套一層 ScrollView。目前為了較好的 UI 體驗(yàn)索烹,更貼
近 Material Design 的設(shè)計(jì)工碾,推薦使用 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>
12.不要在 Android 的 Application 對(duì)象中緩存數(shù)據(jù)百姓≡ǘ睿基礎(chǔ)組件之間的數(shù)據(jù)共享
請(qǐng)使用 Intent 等機(jī)制,也可使用 SharedPreferences 等數(shù)據(jù)持久化機(jī)制垒拢。
13.使用 Toast 時(shí)旬迹,建議定義一個(gè)全局的 Toast 對(duì)象,這樣可以避免連續(xù)顯示
Toast 時(shí)不能取消上一次 Toast 消息的情況求类。即使需要連續(xù)彈出 Toast奔垦,也應(yīng)避免直
接調(diào)用 Toast#makeText。
14.使用 Adapter 的時(shí)候尸疆,如果你使用了 ViewHolder 做緩存椿猎,在 getView()的
方法中無(wú)論這項(xiàng) convertView 的每個(gè)子控件是否需要設(shè)置屬性(比如某個(gè) TextView
設(shè)置的文本可能為 null,某個(gè)按鈕的背景色為透明寿弱,某控件的顏色為透明等)犯眠,都需
要為其顯式設(shè)置屬性(Textview 的文本為空也需要設(shè)置 setText(""),背景透明也需要
設(shè)置)症革,否則在滑動(dòng)的過(guò)程中筐咧,因?yàn)?adapter item 復(fù)用的原因,會(huì)出現(xiàn)內(nèi)容的顯示錯(cuò)
亂噪矛。
@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;
}
五量蕊、進(jìn)程、線程與消息通信
1.不要通過(guò) Intent 在 Android 基礎(chǔ)組件之間傳遞大數(shù)據(jù)(binder transaction
緩存為 1MB)摩疑,可能導(dǎo)致 OOM危融。
2.在 Application 的業(yè)務(wù)初始化代碼加入進(jìn)程判斷,確保只在自己需要的進(jìn)程
初始化雷袋。特別是后臺(tái)進(jìn)程減少不必要的業(yè)務(wù)初始化吉殃。
public class MyApplication extends Application {
@Override
public void onCreate() {
//在所有進(jìn)程中初始化
....
//僅在主進(jìn)程中初始化
if (mainProcess) {
...
}
//僅在后臺(tái)進(jìn)程中初始化
if (bgProcess) {
...
}
}
}
3.新建線程時(shí)辞居,必須通過(guò)線程池提供(AsyncTask 或者 ThreadPoolExecutor
或者其他形式自定義的線程池),不允許在應(yīng)用中自行顯式創(chuàng)建線程蛋勺。
說(shuō)明:
使用線程池的好處是減少在創(chuàng)建和銷毀線程上所花的時(shí)間以及系統(tǒng)資源的開(kāi)銷瓦灶,解
決資源不足的問(wèn)題。如果不使用線程池抱完,有可能造成系統(tǒng)創(chuàng)建大量同類線程而導(dǎo)致
消耗完內(nèi)存或者“過(guò)度切換”的問(wèn)題贼陶。另外創(chuàng)建匿名線程不便于后續(xù)的資源使用分析,
對(duì)性能分析等會(huì)造成困擾巧娱。
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());
//執(zhí)行任務(wù)
executorService.execute(new Runnnable() {
...
});
4.線程池不允許使用 Executors 去創(chuàng)建碉怔,而是通過(guò) ThreadPoolExecutor 的方
式,這樣的處理方式讓寫的同學(xué)更加明確線程池的運(yùn)行規(guī)則禁添,規(guī)避資源耗盡的風(fēng)險(xiǎn)撮胧。
說(shuō)明:
Executors 返回的線程池對(duì)象的弊端如下:
- FixedThreadPool 和 SingleThreadPool : 允許的請(qǐng)求隊(duì)列長(zhǎng)度為
Integer.MAX_VALUE,可能會(huì)堆積大量的請(qǐng)求老翘,從而導(dǎo)致 OOM芹啥; - CachedThreadPool 和 ScheduledThreadPool : 允許的創(chuàng)建線程數(shù)量為
Integer.MAX_VALUE,可能會(huì)創(chuàng)建大量的線程铺峭,從而導(dǎo)致 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());
5.子線程中不能更新界面,更新界面必須在主線程中進(jìn)行卫键,網(wǎng)絡(luò)操作不能在
主線程中調(diào)用傀履。
6.盡量減少不同 APP 之間的進(jìn)程間通信及拉起行為。拉起導(dǎo)致占用系統(tǒng)資源永罚,
影響用戶體驗(yàn)啤呼。
7.新建線程時(shí),定義能識(shí)別自己業(yè)務(wù)的線程名稱呢袱,便于性能優(yōu)化和問(wèn)題排查官扣。
public class MyThread extends Thread {
public MyThread(){
super.setName("ThreadName");
…
}
}
8.禁止在多進(jìn)程之間用 SharedPreferences 共享數(shù)據(jù),雖然可以
(MODE_MULTI_PROCESS)羞福,但官方已不推薦惕蹄。
9.謹(jǐn)慎使用 Android 的多進(jìn)程,多進(jìn)程雖然能夠降低主進(jìn)程的內(nèi)存壓力治专,但
會(huì)遇到如下問(wèn)題:
- 首次進(jìn)入新啟動(dòng)進(jìn)程的頁(yè)面時(shí)會(huì)有延時(shí)的現(xiàn)象(有可能黑屏卖陵、白屏幾秒,是白
屏還是黑屏和新 Activity 的主題有關(guān))张峰; - 應(yīng)用內(nèi)多進(jìn)程時(shí)泪蔫,Application 實(shí)例化多次,需要考慮各個(gè)模塊是否都需要在所
有進(jìn)程中初始化喘批。
六撩荣、文件與數(shù)據(jù)庫(kù)
1.任何時(shí)候不要硬編碼文件路徑铣揉,請(qǐng)使用 Android 文件系統(tǒng) API 訪問(wèn)。
說(shuō)明:
Android 應(yīng)用提供內(nèi)部和外部存儲(chǔ)餐曹,分別用于存放應(yīng)用自身數(shù)據(jù)以及應(yīng)用產(chǎn)生的用
戶數(shù)據(jù)逛拱。可以通過(guò)相關(guān) API 接口獲取對(duì)應(yīng)的目錄台猴,進(jìn)行文件操作朽合。
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;
}
2.當(dāng)使用外部存儲(chǔ)時(shí),必須檢查外部存儲(chǔ)的可用性饱狂。
// 讀/寫檢查
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;
}
3.應(yīng)用間共享文件時(shí)曹步,不要通過(guò)放寬文件系統(tǒng)權(quán)限的方式去實(shí)現(xiàn),而應(yīng)使用
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);
}
4.SharedPreference 中只能存儲(chǔ)簡(jiǎn)單數(shù)據(jù)類型(int箭窜、boolean、String 等)衍腥,
復(fù)雜數(shù)據(jù)類型建議使用文件、數(shù)據(jù)庫(kù)等其他方式存儲(chǔ)纳猫。
public void updateSettings() {
SharedPreferences mySharedPreferences = getSharedPreferences("settings",
Activity.MODE_PRIVATE);
SharedPreferences.Editor editor = mySharedPreferences.edit();
editor.putString("id", "foo");
editor.putString("nick", "bar");
//不要把復(fù)雜數(shù)據(jù)類型轉(zhuǎn)成 String 存儲(chǔ)
editor.apply();
}
5.SharedPreference 提交數(shù)據(jù)時(shí)婆咸,盡量使用 Editor#apply() ,而非
Editor#commit()芜辕。一般來(lái)講尚骄,僅當(dāng)需要確定提交結(jié)果,并據(jù)此有后續(xù)操作時(shí)侵续,才使
用 Editor#commit()倔丈。
說(shuō)明:
SharedPreference 相關(guān)修改使用 apply 方法進(jìn)行提交會(huì)先寫入內(nèi)存,然后異步寫入
磁盤状蜗,commit 方法是直接寫入磁盤需五。如果頻繁操作的話 apply 的性能會(huì)優(yōu)于 commit,
apply 會(huì)將最后修改內(nèi)容寫入磁盤轧坎。但是如果希望立刻獲取存儲(chǔ)操作的結(jié)果宏邮,并據(jù)此
做相應(yīng)的其他操作,應(yīng)當(dāng)使用 commit缸血。
不過(guò)蜜氨,還是推薦commit來(lái)操作,保證數(shù)據(jù)存儲(chǔ)的可靠性捎泻,犧牲部分性能
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");
}
}
6.數(shù)據(jù)庫(kù) Cursor 必須確保使用完后關(guān)閉飒炎,以免內(nèi)存泄漏。
說(shuō)明:
Cursor 是對(duì)數(shù)據(jù)庫(kù)查詢結(jié)果集管理的一個(gè)類笆豁,當(dāng)查詢的結(jié)果集較小時(shí)郎汪,消耗內(nèi)存不
易察覺(jué)赤赊。但是當(dāng)結(jié)果集較大,長(zhǎng)時(shí)間重復(fù)操作會(huì)導(dǎo)致內(nèi)存消耗過(guò)大怒竿,需要開(kāi)發(fā)者在
操作完成后手動(dòng)關(guān)閉 Cursor砍鸠。
數(shù)據(jù)庫(kù) Cursor 在創(chuàng)建及使用時(shí),可能發(fā)生各種異常耕驰,無(wú)論程序是否正常結(jié)束爷辱,必須
在最后確保 Cursor 正確關(guān)閉,以避免內(nèi)存泄漏朦肘。同時(shí)饭弓,如果 Cursor 的使用還牽涉
多線程場(chǎng)景,那么需要自行保證操作同步媒抠。
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();
}
}
}
7.多線程操作寫入數(shù)據(jù)庫(kù)時(shí)弟断,需要使用事務(wù),以免出現(xiàn)同步問(wèn)題趴生。
說(shuō)明:
通過(guò) SQLiteOpenHelper 獲取數(shù)據(jù)庫(kù) SQLiteDatabase 實(shí)例阀趴,Helper 中會(huì)自動(dòng)緩存
已經(jīng)打開(kāi)的 SQLiteDatabase 實(shí)例,單個(gè) App 中應(yīng)使用 SQLiteOpenHelper 的單例
模式確保數(shù)據(jù)庫(kù)連接唯一苍匆。由于 SQLite 自身是數(shù)據(jù)庫(kù)級(jí)鎖刘急,單個(gè)數(shù)據(jù)庫(kù)操作是保證
線程安全的(不能同時(shí)寫入),transaction 是一次原子操作浸踩,因此處于事務(wù)中的操作
是線程安全的叔汁。
若同時(shí)打開(kāi)多個(gè)數(shù)據(jù)庫(kù)連接,并通過(guò)多線程寫入數(shù)據(jù)庫(kù)检碗,會(huì)導(dǎo)致數(shù)據(jù)庫(kù)異常据块,提示
數(shù)據(jù)庫(kù)已被鎖住。
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();
}
}
8.大數(shù)據(jù)寫入數(shù)據(jù)庫(kù)時(shí)折剃,請(qǐng)使用事務(wù)或其他能夠提高 I/O 效率的機(jī)制另假,保證執(zhí)
行速度。
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();
}
}
9.執(zhí)行 SQL 語(yǔ)句時(shí)怕犁,應(yīng)使用 SQLiteDatabase#insert()浪谴、update()、delete()因苹,
不要使用 SQLiteDatabase#execSQL()苟耻,以免 SQL 注入風(fēng)險(xiǎn)。
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);
}
10.如果 ContentProvider 管理的數(shù)據(jù)存儲(chǔ)在 SQL 數(shù)據(jù)庫(kù)中扶檐,應(yīng)該避免將不受
信任的外部數(shù)據(jù)直接拼接在原始 SQL 語(yǔ)句中凶杖。
// 使用一個(gè)可替換參數(shù)
String mSelectionClause = "var = ?";
String[] selectionArgs = {""};
selectionArgs[0] = mUserInput;
七、Bitmap款筑、Drawable與動(dòng)畫
1.加載大圖片或者一次性加載多張圖片智蝠,應(yīng)該在異步線程中進(jìn)行腾么。圖片的加
載,涉及到 IO 操作杈湾,以及 CPU 密集操作解虱,很可能引起卡頓。
class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
...
// 在后臺(tái)進(jìn)行圖片解碼
@Override
protected Bitmap doInBackground(Integer... params) {
final Bitmap bitmap = BitmapFactory.decodeFile("some path");
return bitmap;
}
...
}
2.在 ListView漆撞,ViewPager殴泰,RecyclerView,GirdView 等組件中使用圖片時(shí)浮驳,
應(yīng)做好圖片的緩存悍汛,避免始終持有圖片導(dǎo)致內(nèi)存溢出,也避免重復(fù)創(chuàng)建圖片至会,引起
性能問(wèn)題离咐。建議 使 用 Fresco ( https://github.com/facebook/fresco )、 Glide
(https://github.com/bumptech/glide) 等圖片庫(kù)奉件。
例如使用系統(tǒng) LruCache 緩存宵蛀,參考:
https://developer.android.com/topic/performance/graphics/cache-bitmap.html
private LruCache<String, Bitmap> mMemoryCache;
@Override
protected void onCreate(Bundle savedInstanceState) {
...
// 獲取可用內(nèi)存的最大值,使用內(nèi)存超出這個(gè)值將拋出 OutOfMemory 異常县貌。LruCache 通
過(guò)構(gòu)造函數(shù)傳入緩存值糖埋,以 KB 為單位。
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// 把最大可用內(nèi)存的 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> {
...
// 在后臺(tái)進(jìn)行圖片解碼
@Override
protected Bitmap doInBackground(Integer... params) {
final Bitmap bitmap = decodeSampledBitmapFromResource(getResources(),
params[0], 100, 100));
addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
return bitmap;
}
...
}
3.png 圖片使用 TinyPNG 或者類似工具壓縮處理窃这,減少包體積。對(duì)于我們藍(lán)湖下載切圖時(shí)征候,必須選擇高質(zhì)量壓縮
4.應(yīng)根據(jù)實(shí)際展示需要杭攻,壓縮圖片,而不是直接顯示原圖疤坝。手機(jī)屏幕比較小兆解,
直接顯示原圖,并不會(huì)增加視覺(jué)上的收益跑揉,但是卻會(huì)耗費(fèi)大量寶貴的內(nèi)存锅睛。
public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
int reqWidth, int reqHeight) {
// 首先通過(guò) inJustDecodeBounds=true 獲得圖片的尺寸
final BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(res, resId, options);
// 然后根據(jù)圖片分辨率以及我們實(shí)際需要展示的大小,計(jì)算壓縮率
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// 設(shè)置壓縮率历谍,并解碼
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
5.使用完畢的圖片现拒,應(yīng)該及時(shí)回收,釋放寶貴的內(nèi)存望侈。
Bitmap bitmap = null;
loadBitmapAsync(new OnResult(result){
bitmap = result;
});
...使用該 bitmap...
// 使用結(jié)束印蔬,在 2.3.3 及以下需要調(diào)用 recycle()函數(shù),在 2.3.3 以上 GC 會(huì)自動(dòng)管理脱衙,除非你明確不需要再用侥猬。
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.GINGERBREAD_MR1) {
bitmap.recycle();
}
bitmap = null;
6.在 Activity#onPause()或 Activity#onStop()回調(diào)中例驹,關(guān)閉當(dāng)前 activity 正在執(zhí)
行的的動(dòng)畫。
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() {
//頁(yè)面退出退唠,及時(shí)清理動(dòng)畫資源
mImageView.clearAnimation()
}
}
7.在動(dòng)畫或者其他異步任務(wù)結(jié)束時(shí)鹃锈,應(yīng)該考慮回調(diào)時(shí)刻的環(huán)境是否還支持業(yè)
務(wù)處理。例如 Activity 的 onStop()函數(shù)已經(jīng)執(zhí)行瞧预,且在該函數(shù)中主動(dòng)釋放了資源屎债,
此時(shí)回調(diào)中如果不做判斷就會(huì)空指針崩潰。
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);
}
}
8.使用 inBitmap 重復(fù)利用內(nèi)存空間松蒜,避免重復(fù)開(kāi)辟新內(nèi)存扔茅。
public static Bitmap decodeSampledBitmapFromFile(String filename, int reqWidth, int
reqHeight, ImageCache cache) {
final BitmapFactory.Options options = new BitmapFactory.Options();
...
BitmapFactory.decodeFile(filename, options);
...
// 如果在 Honeycomb 或更新版本系統(tǒng)中運(yùn)行,嘗試使用 inBitmap
if (Utils.hasHoneycomb()) {
addInBitmapOptions(options, cache);
}
...
return BitmapFactory.decodeFile(filename, options);
}
private static void addInBitmapOptions(BitmapFactory.Options options,
ImageCache cache) {
// inBitmap 只處理可變的位圖秸苗,所以強(qiáng)制返回可變的位圖
options.inMutable = true;
if (cache != null) {
Bitmap inBitmap = cache.getBitmapFromReusableSet(options);
if (inBitmap != null) {
options.inBitmap = inBitmap;
}
}
}
9.使用 RGB_565 代替 RGB_888召娜,在不怎么降低視覺(jué)效果的前提下,減少內(nèi)
存占用惊楼。
說(shuō)明:
android.graphics.Bitmap.Config 類中關(guān)于圖片顏色的存儲(chǔ)方式定義:
- ALPHA_8 代表 8 位 Alpha 位圖玖瘸;
- ARGB_4444 代表 16 位 ARGB 位圖;
- ARGB_8888 代表 32 位 ARGB 位圖檀咙;
- RGB_565 代表 8 位 RGB 位圖捕捂。
位圖位數(shù)越高,存儲(chǔ)的顏色信息越多狈茉,圖像也就越逼真蒿往。大多數(shù)場(chǎng)景使用的是
ARGB_8888 和 RGB_565,RGB_565 能夠在保證圖片質(zhì)量的情況下大大減少內(nèi)存
的開(kāi)銷棕诵,是解決 OOM 的一種方法裁良。
但是一定要注意 RGB_565 是沒(méi)有透明度的,如果圖片本身需要保留透明度校套,那么
就不能使用 RGB_565价脾。
Config config = drawableSave.getOpacity() != PixelFormat.OPAQUE ? Config.ARGB_8565 :
Config.RGB_565;
Bitmap bitmap = Bitmap.createBitmap(w, h, config);
10.盡量減少 Bitmap(BitmapDrawable)的使用,盡量使用純色(ColorDrawable)笛匙、
漸變色(GradientDrawable)侨把、StateSelector(StateListDrawable)等與 Shape 結(jié)
合的形式構(gòu)建繪圖。
11.謹(jǐn)慎使用 gif 圖片妹孙,注意限制每個(gè)頁(yè)面允許同時(shí)播放的 gif 圖片秋柄,以及單個(gè)
gif 圖片的大小。
12.大圖片資源不要直接打包到 apk蠢正,可以考慮通過(guò)文件倉(cāng)庫(kù)遠(yuǎn)程下載华匾,減小包
體積。可不考慮
13.根據(jù)設(shè)備性能,選擇性開(kāi)啟復(fù)雜動(dòng)畫蜘拉,以實(shí)現(xiàn)一個(gè)整體較優(yōu)的性能和體驗(yàn)萨西;
14.在有強(qiáng)依賴 onAnimationEnd 回調(diào)的交互時(shí),如動(dòng)畫播放完畢才能操作頁(yè)
面 旭旭, onAnimationEnd 可能會(huì)因各種異常沒(méi)被回調(diào) (參考:
https://stackoverflow.com/questions/5474923/onanimationend-is-not-getting-called-onanimationstart-works-fine )谎脯,建議加上超時(shí)保護(hù)或通過(guò) 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.當(dāng) View Animation 執(zhí)行結(jié)束時(shí)持寄,調(diào)用 View.clearAnimation()釋放相關(guān)資源源梭。
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.禁止使用常量初始化矢量參數(shù)構(gòu)建 IvParameterSpec稍味,建議 IV 通過(guò)隨機(jī)方
式產(chǎn)生废麻。
說(shuō)明:
使用常量初始化向量,密碼文本的可預(yù)測(cè)性會(huì)高得多模庐,容易受到字典式攻擊烛愧。iv 的
作用主要是用于產(chǎn)生密文的第一個(gè) block,以使最終生成的密文產(chǎn)生差異(明文相同
的情況下)掂碱,使密碼攻擊變得更為困難怜姿。
byte[] rand = new byte[16];
SecureRandom r = new SecureRandom();
r.nextBytes(rand);
IvParameterSpec iv = new IvParameterSpec(rand);
2.將 android:allowbackup 屬性必須設(shè)置為 false,阻止應(yīng)用數(shù)據(jù)被導(dǎo)出疼燥。
說(shuō)明:
android:allowBackup 原本是 Android 提供的 adb 調(diào)試功能沧卢,如果設(shè)置為 true,
可以導(dǎo)出應(yīng)用數(shù)據(jù)備份并在任意設(shè)備上恢復(fù)醉者。這對(duì)應(yīng)用安全性和用戶數(shù)據(jù)隱私構(gòu)成
極大威脅但狭,所以必須設(shè)置為 false,防止數(shù)據(jù)泄露撬即。
<application
android:allowBackup="false"
android:largeHeap="true"
android:icon="@drawable/test_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
3.如果使用自定義 HostnameVerifier 實(shí)現(xiàn)類立磁,必須在 verify()方法中校驗(yàn)服務(wù)
器主機(jī)名的合法性,否則可能受到中間人攻擊搞莺。
說(shuō)明:
在與服務(wù)器建立 https 連接時(shí),如果 URL 的主機(jī)名和服務(wù)器的主機(jī)名不匹配掂咒,則
可通過(guò)該回調(diào)接口來(lái)判斷是否應(yīng)該允許建立連接才沧。如果回調(diào)內(nèi)實(shí)現(xiàn)不恰當(dāng),沒(méi)有有
效校驗(yàn)主機(jī)名绍刮,甚至默認(rèn)接受所有主機(jī)名温圆,會(huì)大大增加安全風(fēng)險(xiǎn)。
HostnameVerifier hnv = new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
// 不做校驗(yàn)孩革,接受任意域名服務(wù)器
return true;
}
};
HttpsURLConnection.setDefaultHostnameVerifier(hnv);
4.如果使用自定義 X509TrustManager 實(shí)現(xiàn)類岁歉,必須在 checkServerTrusted()
方法中校驗(yàn)服務(wù)端證書的合法性,否則可能受到中間人攻擊。
說(shuō)明:
常見(jiàn)誤區(qū)是 checkServerTrusted()方法根本沒(méi)有實(shí)現(xiàn)锅移,這將導(dǎo)致 X509TrustManager
形同虛設(shè)熔掺。該方法中需要實(shí)現(xiàn)完備的校驗(yàn)邏輯,對(duì)于證書錯(cuò)誤拋出
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);
}
}
};
5.在 SDK 支持的情況下置逻,Android 應(yīng)用必須使用 V2 簽名,這將對(duì) APK 文
件的修改做更多的保護(hù)备绽。
6.所有的 Android 基本組件(Activity券坞、Service、BroadcastReceiver肺素、
ContentProvider 等)都不應(yīng)在沒(méi)有嚴(yán)格權(quán)限控制的情況下恨锚,將 android:exported 設(shè)
置為 true。
7.WebView 應(yīng)設(shè)置 WebView#getSettings()#setAllowFileAccess(false)倍靡、
WebView#getSettings()#setAllowFileAccessFromFileURLs(false) 猴伶、
WebView#getSettings()#setAllowUniversalAccessFromFileURLs(false),阻止 file
scheme URL 的訪問(wèn)菌瘫。
8.不要把敏感信息打印到 log 中蜗顽。
說(shuō)明:
在開(kāi)發(fā)過(guò)程中,為了方便調(diào)試雨让,通常會(huì)使用 log 函數(shù)輸出一些關(guān)鍵流程的信息雇盖,這
些信息中通常會(huì)包含敏感內(nèi)容,讓攻擊者更加容易了解 APP 內(nèi)部結(jié)構(gòu)栖忠,方便破解和
攻擊崔挖,甚至直接獲取到有價(jià)值的敏感信息。
9.確保應(yīng)用發(fā)布版本的 android:debuggable 屬性設(shè)置為 false庵寞。
10.本地加密秘鑰不能硬編碼在代碼中狸相,更不能使用 SharedPreferences 等本
地持久化機(jī)制存儲(chǔ)。應(yīng)選擇 Android 自身的秘鑰庫(kù)(KeyStore)機(jī)制或者其他安全
性更高的安全解決方案保存捐川。
說(shuō)明:
應(yīng)用程序在加解密時(shí)脓鹃,使用硬編碼在程序中的密鑰,攻擊者通過(guò)反編譯拿到密鑰可
以輕易解密 APP 通信數(shù)據(jù)古沥。
11.addJavascriptInterface() 可以添加 JS 對(duì)本地 Java 方法的調(diào)用瘸右,但這本身
會(huì)導(dǎo)致惡意代碼的攻擊。在 Android 4.2(API Level 17)以下岩齿,不應(yīng)再使用這樣的
調(diào)用方式太颤。在 Android 4.2 及以上,需要對(duì)本地被遠(yuǎn)程調(diào)用的方法顯式添加
@JavascriptInterface annotation盹沈。
12.使用 Android 的 AES/DES/DESede 加密算法時(shí)龄章,不要使用 ECB 加密模式,
應(yīng)使用 CBC 或 CFB 加密模式。
說(shuō)明:
加密模式有 ECB做裙、CBC岗憋、CFB、OFB 等菇用,其中 ECB 的安全性較弱澜驮,如果使用固
定的密鑰,相同的明文將會(huì)生成相同的密文惋鸥,容易受到字典攻擊杂穷,建議使用 CBC、
CFB 或 OFB 等模式卦绣。
- ECB:Electronic codebook耐量,電子密碼本模式
- CBC:Cipher-block chaining,密碼分組鏈接模式
- CFB:Cipher feedback滤港,密文反饋模式
- OFB:Output feedback廊蜒,輸出反饋模式
13.Android APP 在 HTTPS 通信中,驗(yàn)證策略需要改成嚴(yán)格模式溅漾。
說(shuō)明:
Android APP 在 HTTPS 通信中山叮,使用 ALLOW_ALL_HOSTNAME_VERIFIER,表
示允許和所有的 HOST 建立 SSL 通信添履,這會(huì)存在中間人攻擊的風(fēng)險(xiǎn)屁倔,最終導(dǎo)致敏感
信息可能會(huì)被劫持,以及其他形式的攻擊暮胧。
ALLOW_ALL_HOSTNAME_VERIFIER 關(guān)閉 host 驗(yàn)證锐借,允許和所有的 host 建立
SSL 通信,BROWSER_COMPATIBLE_HOSTNAME_VERIFIER 和瀏覽器兼容的
驗(yàn)證策略往衷,即通配符能夠匹配所有子域名 钞翔,STRICT_HOSTNAME_VERIFIER 嚴(yán)
格匹配模式,hostname 必須匹配第一個(gè) CN 或者任何一個(gè) subject-alts席舍,以上例子
使用了 ALLOW_ALL_HOSTNAME_VERIFIER布轿,需要改成 STRICT_HOSTNAME_
VERIFIER。
14.在 Android 4.2(API Level 17)及以上来颤,對(duì)安全性要求較高的應(yīng)用可在 Activity
中汰扭,對(duì) Activity 所關(guān)聯(lián)的 Window 應(yīng)用 WindowManager.LayoutParams.FLAG_
SECURE,防止被截屏脚曾、錄屏东且。但要注意的是启具,一個(gè) Activity 關(guān)聯(lián)的 Window 可
能不止一個(gè)本讥,如果使用了 Dialog / DialogFragment 等控件彈出對(duì)話框,它們本身
也會(huì)創(chuàng)建一個(gè)新的 Window,也一樣需要保護(hù)拷沸。
15.zip 中不要包含 ../../file 這樣的路徑色查,可能被篡改目錄結(jié)構(gòu),造成攻擊撞芍。
說(shuō)明:
當(dāng) zip 壓縮包中允許存在"../"的字符串秧了,攻擊者可以利用多個(gè)"../"在解壓時(shí)改變 zip 文
件存放的位置,當(dāng)文件已經(jīng)存在是就會(huì)進(jìn)行覆蓋序无,如果覆蓋掉的文件是 so验毡、dex 或
者 odex 文件,就有可能造成嚴(yán)重的安全問(wèn)題帝嗡。
對(duì)路徑進(jìn)行判斷晶通,存在".."時(shí)拋出異常。
//對(duì)重要的 Zip 壓縮包文件進(jìn)行數(shù)字簽名校驗(yàn)哟玷,校驗(yàn)通過(guò)才進(jìn)行解壓
String entryName = entry.getName();
if (entryName.contains("..")){
throw new Exception("unsecurity zipfile!");
}
16.MD5 和 SHA-1狮辽、SHA-256 等常用算法是 Hash 算法,有一定的安全性巢寡,
但不能代替加密算法喉脖。敏感信息的存儲(chǔ)和傳輸,需要使用專業(yè)的加密機(jī)制抑月。
九树叽、其他
1.不能使用 System.out.println 打印 log。
Log.d(TAG, "Some Android Debug info ...");
2.Log 的 tag 不能是" "爪幻。
說(shuō)明:
日志的 tag 是空字符串沒(méi)有任何意義菱皆,也不利于過(guò)濾日志。
private static String TAG = "LoginActivity";
Log.e(TAG, "Login failed!");