Android 資源文件命名與使用
【推薦】資源文件需帶模塊前綴漱抓。
【推薦】layout 文件的命名方式黄刚。
Activity
的layout 以module_activity
開頭
Fragment
的layout 以module_fragment
開頭
Dialog
的layout 以module_dialog
開頭
include
的layout 以module_include
開頭
ListView
的行l(wèi)ayout 以module_list_item
開頭
RecyclerView
的item layout 以module_recycle_item
開頭
GridView
的item layout 以module_grid_item
開頭【推薦】drawable 資源名稱以
小寫單詞+下劃線
的方式命名咆繁,根據(jù)分辨率不同存放在
不同的drawable 目錄下蚓挤,如果介意包大小建議只使用一套,系統(tǒng)去進行縮放搬素。采用
規(guī)則如下:
模塊名_業(yè)務功能描述_控件描述_控件狀態(tài)限定詞
如:module_login_btn_pressed
,module_tabs_icon_home_normal
【推薦】anim 資源名稱以
小寫單詞+下劃線
的方式命名境输,采用以下規(guī)則:
模塊名_邏輯名稱_[方向|序號]
Tween 動畫(使用簡單圖像變換的動畫剂跟,例如縮放蜕衡、平移)資源:盡可能以通用的
動畫名稱命名,如module_fade_in
,module_fade_out
,module_push_down_in (動 畫+方向)
。
Frame 動畫(按幀順序播放圖像的動畫)資源:盡可能以模塊+功能命名+序號
。如
module_loading_grey_001
陶衅。【推薦】color 資源使用
#AARRGGBB
格式,寫入module_colors.xml 文件中直晨,命名
格式采用以下規(guī)則:
模塊名_邏輯名稱_顏色
如:
<color name="module_btn_bg_color">#33b5e5e5</color>
【推薦】dimen 資源以
小寫單詞+下劃線
方式命名搀军,寫入module_dimens.xml 文件中,
采用以下規(guī)則:
模塊名_描述信息
如:
<dimen name="module_horizontal_line_height">1dp</dimen>
【推薦】style 資源采用
父style 名稱.當前style 名稱
方式命名勇皇,寫入
module_styles.xml 文件中罩句,首字母大寫。如:
<style name="ParentTheme.ThisActivityTheme"> … </style>
【推薦】string資源文件或者文本用到字符需要全部寫入module_strings.xml 文件中敛摘,
字符串以小寫單詞+下劃線
的方式命名门烂,采用以下規(guī)則:
模塊名_邏輯名稱
如: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.【推薦】圖片根據(jù)其分辨率氓润,放在不同屏幕密度的drawable 目錄下管理,否則可能
在低密度設備上導致內(nèi)存占用增加薯鳍,又可能在高密度設備上導致圖片顯示不夠清晰咖气。
說明:
為了支持多種屏幕尺寸和密度,Android 提供了多種通用屏幕密度來適配挖滤。常用的
如下崩溪。
ldpi - 120dpi
mdpi - 160dpi
hdpi - 240dpi
xhdpi - 320dpi
xxhdpi - 480dpi
xxxhdpi - 640dpi
Android 的屏幕分辨率和密度并不存在嚴格的對應關系,應盡量避免直接基于分辨
率來開發(fā)斩松,而是通過適配不同的屏幕密度來保證控件和圖片的顯示效果伶唯。不同密度
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 間的數(shù)據(jù)通信闽颇,對于數(shù)據(jù)量比較大的,避免使用
Intent + Parcelable
的方式寄锐,可以考慮EventBus
等替代方案兵多,以免造成TransactionTooLargeException
。【推薦】
Activity#onSaveInstanceState()
方法不是Activity 生命周期方法橄仆,也不保證
一定會被調(diào)用剩膘。它是用來在Activity 被意外銷毀時保存UI 狀態(tài)的,只能用于保存臨
時性數(shù)據(jù)盆顾,例如UI 控件的屬性等怠褐,不能跟數(shù)據(jù)的持久化存儲混為一談。持久化存儲
應該在Activity#onPause()/onStop()
中實行您宪。【強制】Activity 間通過隱式Intent 的跳轉(zhuǎn)奈懒,在發(fā)出Intent 之前必須通過
resolveActivity
檢查奠涌,避免找不到合適的調(diào)用組件,造成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()
方法中執(zhí)行耗時操作溜畅,如果確
實有需求,應改用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()
中執(zhí)行耗時操作慈格,如果有耗時工作,
應該創(chuàng)建IntentService 完成遥金,而不應該在BroadcastReceiver 內(nèi)創(chuàng)建子線程去做浴捆。
說明:
由于該方法是在主線程執(zhí)行,如果執(zhí)行耗時操作會導致UI 不流暢稿械⊙⌒海可以使用
IntentService
、創(chuàng)建HandlerThread
或者調(diào)用Context#registerReceiver (BroadcastReceiver, IntentFilter, String, Handler)
方法等方式美莫,在其他Wroker 線程
執(zhí)行onReceive
方法滔金。BroadcastReceiver#onReceive()
方法耗時超過10 秒鐘,可
能會被系統(tǒng)殺死茂嗓。
正例:
IntentFilter filter = new IntentFilter();
filter.addAction(LOGIN_SUCCESS);
this.registerReceiver(mBroadcastReceiver, filter);
mBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Intent userHomeIntent = new Intent();
userHomeIntent.setClass(this, 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()
發(fā)送的隱式廣播會被所有感興趣的receiver 接收述吸,惡
意應用注冊監(jiān)聽該廣播的receiver 可能會獲取到Intent 中傳遞的敏感信息忿族,并進行
其他危險操作。如果發(fā)送的廣播為使用Context#sendOrderedBroadcast()
方法發(fā)送
的有序廣播蝌矛,優(yōu)先級較高的惡意receiver 可能直接丟棄該廣播道批,造成服務不可用,
或者向廣播結(jié)果塞入惡意數(shù)據(jù)入撒。
如果廣播僅限于應用內(nèi)隆豹,則可以使用LocalBroadcastManager#sendBroadcast()
實
現(xiàn),避免敏感信息外泄和Intent 攔截的風險茅逮。
正例:
Intent intent = new Intent("my-sensitive-event");
intent.putExtra("event", "this is a test event");
LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
反例:
Intent intent = new Intent();
v1.setAction("com.sample.action.server_running");
v1.putExtra("local_ip", v0.h);
v1.putExtra("port", v0.i);
v1.putExtra("code", v0.g);
v1.putExtra("connected", v0.s);
v1.putExtra("pwd_predefined", v0.r);
if (!TextUtils.isEmpty(v0.t)) {
v1.putExtra("connected_usr", v0.t);
}
context.sendBroadcast(v1);
以上廣播可能被其他應用的如下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()
內(nèi)調(diào)用。
不要隨意使用FragmentTransaction#commitAllowingStateLoss()
來代替献雅,任何
commitAllowingStateLoss()
的使用必須經(jīng)過code review
碉考,確保無負面影響。
說明:
Activity 可能因為各種原因被銷毀挺身, Android 支持頁面被銷毀前通過
Activity#onSaveInstanceState()
保存自己的狀態(tài)侯谁。但如果
FragmentTransaction.commit()
發(fā)生在Activity 狀態(tài)保存之后,就會導致Activity 重
建、恢復狀態(tài)時無法還原頁面狀態(tài)墙贱,從而可能出錯热芹。為了避免給用戶造成不好的體驗,系統(tǒng)會拋出IllegalStateExceptionStateLoss
異常惨撇。推薦的做法是在Activity 的
onPostResume()
或onResumeFragments() ( 對FragmentActivity )
里執(zhí)行
FragmentTransaction.commit()
伊脓,如有必要也可在onCreate()里執(zhí)行。不要隨意改用
FragmentTransaction.commitAllowingStateLoss()
或者直接使用try-catch 避免
crash串纺,這不是問題的根本解決之道丽旅,當且僅當你確認Activity 重建椰棘、恢復狀態(tài)時纺棺,
本次commit 丟失不會造成影響時才可這么做。
正例:
public class MainActivity extends FragmentActivity {
FragmentManager fragmentManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main2);
fragmentManager = getSupportFragmentManager();
FragmentTransaction ft = fragmentManager.beginTransaction();
MyFragment fragment = new MyFragment();
ft.replace(R.id.fragment_container, fragment);
ft.commit();
}
}
反例:
public 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()
內(nèi)執(zhí)行釋放資源的工作邪狞,例如一些工作線程的
銷毀和停止祷蝌,因為onDestroy()執(zhí)行的時機可能較晚》浚可根據(jù)實際需要巨朦,在
Activity#onPause()/onStop()
中結(jié)合isFinishing()
的判斷來執(zhí)行。【推薦】如非必須剑令,避免使用嵌套的Fragment糊啡。
說明:
嵌套Fragment 是在Android API 17添加到SDK以及Support 庫中的功能,F(xiàn)ragment
嵌套使用會有一些坑吁津,容易出現(xiàn)bug棚蓄,比較常見的問題有如下幾種:
- onActivityResult()方法的處理錯亂,內(nèi)嵌的Fragment 可能收不到該方法的回調(diào)碍脏,
需要由宿主Fragment 進行轉(zhuǎn)發(fā)處理梭依; - 突變動畫效果;
- 被繼承的setRetainInstance()典尾,導致在Fragment 重建時多次觸發(fā)不必要的邏
輯役拴。
非必須的場景盡可能避免使用嵌套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,
保證應用的安全性褥紫。如果確實需要使用隱式調(diào)用淤击,則可為Service 提供Intent Filter
并從Intent 中排除相應的組件名稱,但必須搭配使用Intent#setPackage()
方法設置
Intent 的指定包名故源,這樣可以充分消除目標服務的不確定性污抬。
11.【推薦】Service 需要以多線程來并發(fā)處理多個啟動請求,建議使用IntentService,
可避免各種復雜的設置印机。
說明:
Service 組件一般運行主線程矢腻,應當避免耗時操作,如果有耗時操作應該在Worker
線程執(zhí)行射赛《喔蹋可以使用IntentService 執(zhí)行后臺任務。
正例:
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.【推薦】對于只用于應用內(nèi)的廣播楣责,優(yōu)先使用LocalBroadcastManager
來進行注冊
和發(fā)送竣灌,LocalBroadcastManager 安全性更好,同時擁有更高的運行效率秆麸。
說明:
對于使用Context#sendBroadcast()
等方法發(fā)送全局廣播的代碼進行提示初嘹。如果該廣
播僅用于應用內(nèi),則可以使用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 方法執(zhí)行結(jié)束后才會創(chuàng)建(onCreate)或恢復
(onRestart)別的Activity,所以在onPause 方法中不適合做耗時較長的工作房铭,這
會影響到頁面之間的跳轉(zhuǎn)效率驻龟。【強制】Activity 或者Fragment 中動態(tài)注冊BroadCastReceiver 時,
registerReceiver()
和unregisterReceiver()
要成對出現(xiàn)缸匪。
說明:
如果registerReceiver()
和unregisterReceiver()
不成對出現(xiàn)翁狐,則可能導致已經(jīng)注冊的
receiver 沒有在合適的時機注銷,導致內(nèi)存泄漏凌蔬,占用內(nèi)存空間露懒,加重SystemService
負擔。
部分華為的機型會對receiver 進行資源管控龟梦,單個應用注冊過多receiver 會觸發(fā)管
控模塊拋出異常隐锭,應用直接崩潰。
正例:
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 的生命周期不對應计贰,可能出現(xiàn)多次onResume 造成receiver 注冊多個钦睡,但
最終只注銷一個,其余receiver 產(chǎn)生內(nèi)存泄漏躁倒。
15.【強制】Android 基礎組件如果使用隱式調(diào)用荞怒,應在 AndroidManifest.xml 中使用
<intent-filter> 或在代碼中使用 IntentFilter 增加過濾。
說明:
如果瀏覽器支持Intent Scheme Uri 語法秧秉,如果過濾不當褐桌,那么惡意用戶可能通過瀏
覽器js 代碼進行一些惡意行為,比如盜取cookie 等象迎。如果使用了Intent.parseUri
函數(shù)荧嵌,獲取的intent 必須嚴格過濾呛踊。
正例:
// 將intent scheme URL 轉(zhuǎn)換為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谭网,可以有效降低嵌套數(shù)。
說明:
Android 應用頁面上任何一個View 都需要經(jīng)過 measure赃春、layout愉择、draw 三個步驟
才能被正確的渲染。從xml layout 的頂部節(jié)點開始進行measure织中,每個子節(jié)點都需
要向自己的父節(jié)點提供自己的尺寸來決定展示的位置锥涕,在此過程中可能還會重新
measure(由此可能導致measure 的時間消耗為原來的2-3 倍)。節(jié)點所處位置越深狭吼,
嵌套帶來的measure 越多层坠,計算就會越費時。這就是為什么扁平的View 結(jié)構(gòu)會性
能更好搏嗡。
同時窿春,頁面擁上的View 越多拉一,measure采盒、layout、draw 所花費的時間就越久蔚润。要縮
短這個時間磅氨,關鍵是保持View 的樹形結(jié)構(gòu)盡量扁平,而且要移除所有不需要渲染的
View嫡纠。理想情況下烦租,總共的measure,layout除盏,draw 時間應該被很好的控制在16ms
以內(nèi)叉橱,以保證滑動屏幕時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);
}
【推薦】源文件統(tǒng)一采用
UTF-8
的形式進行編碼探膊。【強制】禁止在非UI 線程進行View 相關操作。
【推薦】文本大小使用單位
dp
待榔,View 大小使用單位dp逞壁。對于TextView,如果在文
字大小確定的情況下推薦使用wrap_content 布局避免出現(xiàn)文字顯示不全的適配問
題。
說明:
之所以文本大小也推薦使用dp 而非sp腌闯,因為sp 是Android 早期推薦使用的袭灯,但其
實sp 不僅和dp 一樣受屏幕密度的影響,還受到系統(tǒng)設置里字體大小的影響绑嘹,所以
使用dp 對于應用開發(fā)會更加保證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
來優(yōu)化布局擅腰,盡可能多的減少UI
布局層級蟋恬,推薦使用FrameLayout
,LinearLayout
趁冈、RelativeLayout
次之歼争。【推薦】在需要時刻刷新某一區(qū)域的組件時,建議通過以下方式避免引發(fā)全局layout
刷新:
- 設置固定的View 大小的寬高渗勘,如倒計時組件等沐绒;
- 調(diào)用View 的layout 方法修改位置,如彈幕組件等旺坠;
- 通過修改Canvas 位置并且調(diào)用invalidate(int l, int t, int r, int b)等方式限定刷新
區(qū)域乔遮; - 通過設置一個是否允許requestLayout 的變量,然后重寫控件的requestlayout取刃、
onSizeChanged 方法蹋肮, 判斷控件的大小沒有改變的情況下, 當進入
requestLayout 的時候,直接返回而不調(diào)用super 的requestLayout 方法。
- 【推薦】不能在Activity 沒有完全顯示時顯示PopupWindow 和Dialog究恤。
說明:
Android Activity 創(chuàng)建時的生命周期,按照onCreate() -> onStart() -> onResume() -> onAttachedToWindow() -> onWindowFocusChanged()
的順序漆魔, 其中在
Activity#onAttachedToWindow() 時,Activity 會與它的 Window 關聯(lián)啦膜,這時 UI 才
會開始繪制有送,在 Activity#onWindowFocusChanged() 時,UI 才變成可交互狀態(tài)僧家,
可以提示用戶使用雀摘。如果在 Window 未關聯(lián)時就創(chuàng)建對話框,UI 可能顯示異常八拱。
推薦的做法是在 Activity#onAttachedToWindow() 之后( 其實最好是
Activity#onWindowFocusChanged() 之后)才創(chuàng)建對話框阵赠。
10.【推薦】盡量不要使用AnimationDrawable
涯塔,它在初始化的時候就將所有圖片加載
到內(nèi)存中,特別占內(nèi)存清蚀,并且還不能釋放匕荸,釋放之后下次進入再次加載時會報錯。
說明:
Android 的幀動畫可以使用AnimationDrawable 實現(xiàn)枷邪,但是如果你的幀動畫中如果
包含過多幀圖片榛搔,一次性加載所有幀圖片所導致的內(nèi)存消耗會使低端機發(fā)生OOM
異常。幀動畫所使用的圖片要注意降低內(nèi)存消耗东揣,當圖片比較大時践惑,容易出現(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>
反例:
<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 都加載到內(nèi)存中,要消耗巨大的內(nèi)存和cpu 去繪制圖
面芥吟。
說明:
ScrollView 中嵌套List 或RecyclerView 的做法官方明確禁止侦铜。除了開發(fā)過程中遇到
的各種視覺和交互問題,這種做法對性能也有較大損耗钟鸵。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 對象中緩存數(shù)據(jù)烈掠。基礎組件之間的數(shù)據(jù)共享
請使用Intent 等機制缸托,也可使用SharedPreferences 等數(shù)據(jù)持久化機制左敌。
反例:
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 對象俐镐,這樣可以避免連續(xù)顯示
Toast 時不能取消上一次Toast 消息的情況矫限。即使需要連續(xù)彈出Toast,也應避免直
接調(diào)用Toast#makeText佩抹。
14.【強制】使用Adapter 的時候叼风,如果你使用了ViewHolder 做緩存,在getView()的
方法中無論這項convertView 的每個子控件是否需要設置屬性(比如某個TextView
設置的文本可能為null棍苹,某個按鈕的背景色為透明无宿,某控件的顏色為透明等),都需
要為其顯式設置屬性(Textview 的文本為空也需要設置setText("")枢里,背景透明也需要
設置)孽鸡,否則在滑動的過程中蹂午,因為adapter item 復用的原因,會出現(xiàn)內(nèi)容的顯示錯
亂彬碱。
正例:
@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 基礎組件之間傳遞大數(shù)據(jù)(binder transaction
緩存為1MB),可能導致OOM巷疼。【強制】在Application 的業(yè)務初始化代碼加入進程判斷晚胡,確保只在自己需要的進程
初始化。特別是后臺進程減少不必要的業(yè)務初始化嚼沿。
正例:
public class MyApplication extends Application {
@Override
public void onCreate() {
//在所有進程中初始化
....
//僅在主進程中初始化
if (mainProcess) {
...
}
//僅在后臺進程中初始化
if (bgProcess) {
...
}
}
}
- 【強制】新建線程時搬泥,必須通過線程池提供(AsyncTask 或者ThreadPoolExecutor或者其他形式自定義的線程池),不允許在應用中自行顯式創(chuàng)建線程伏尼。
說明:
使用線程池的好處是減少在創(chuàng)建和銷毀線程上所花的時間以及系統(tǒng)資源的開銷忿檩,解決資源不足的問題。如果不使用線程池爆阶,有可能造成系統(tǒng)創(chuàng)建大量同類線程而導致消耗完內(nèi)存或者“過度切換”的問題燥透。另外創(chuàng)建匿名線程不便于后續(xù)的資源使用分析,
對性能分析等會造成困擾辨图。
正例:
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í)行任務
executorService.execute(new Runnnable() {
...
});
反例:
new Thread(new Runnable() {
@Override
public void run() {
//操作語句
...
}
}).start();
- 【強制】線程池不允許使用Executors 去創(chuàng)建班套,而是通過ThreadPoolExecutor 的方
式,這樣的處理方式讓寫的同學更加明確線程池的運行規(guī)則故河,規(guī)避資源耗盡的風險吱韭。
說明:
Executors 返回的線程池對象的弊端如下:
- FixedThreadPool 和SingleThreadPool : 允許的請求隊列長度為Integer.MAX_VALUE,可能會堆積大量的請求鱼的,從而導致OOM理盆;
- CachedThreadPool 和ScheduledThreadPool : 允許的創(chuàng)建線程數(shù)量為Integer.MAX_VALUE,可能會創(chuàng)建大量的線程凑阶,從而導致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();
【強制】子線程中不能更新界面,更新界面必須在主線程中進行宙橱,網(wǎng)絡操作不能在
主線程中調(diào)用姨俩。【推薦】盡量減少不同APP 之間的進程間通信及拉起行為。拉起導致占用系統(tǒng)資源师郑,
影響用戶體驗环葵。【推薦】新建線程時,定義能識別自己業(yè)務的線程名稱宝冕,便于性能優(yōu)化和問題排查张遭。
正例:
public class MyThread extends Thread {
public MyThread(){
super.setName("ThreadName");
…
}
}
【推薦】ThreadPoolExecutor 設置線程存活時間(setKeepAliveTime),確扁剩空閑時
線程能被釋放帝璧。【推薦】禁止在多進程之間用SharedPreferences 共享數(shù)據(jù)先誉, 雖然可以
(MODE_MULTI_PROCESS),但官方已不推薦的烁。
10.【推薦】謹慎使用Android 的多進程褐耳,多進程雖然能夠降低主進程的內(nèi)存壓力,但
會遇到如下問題:
- 首次進入新啟動進程的頁面時會有延時的現(xiàn)象(有可能黑屏渴庆、白屏幾秒铃芦,是白
屏還是黑屏和新Activity 的主題有關); - 應用內(nèi)多進程時襟雷,Application 實例化多次刃滓,需要考慮各個模塊是否都需要在所
有進程中初始化。
文件與數(shù)據(jù)庫
- 【強制】任何時候不要硬編碼文件路徑耸弄,請使用Android 文件系統(tǒng)API 訪問咧虎。
說明:
Android 應用提供內(nèi)部和外部存儲,分別用于存放應用自身數(shù)據(jù)以及應用產(chǎn)生的用
戶數(shù)據(jù)计呈∨樗校可以通過相關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 更容易出現(xiàn)適配問題
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;
}
- 【強制】應用間共享文件時,不要通過放寬文件系統(tǒng)權(quán)限的方式去實現(xiàn)善镰,而應使用
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 中只能存儲簡單數(shù)據(jù)類型(int媳禁、boolean眠副、String 等),
復雜數(shù)據(jù)類型建議使用文件竣稽、數(shù)據(jù)庫等其他方式存儲。
正例:
public void updateSettings() {
SharedPreferences mySharedPreferences = getSharedPreferences("settings",Activity.MODE_PRIVATE);
SharedPreferences.Editor editor = mySharedPreferences.edit();
editor.putString("id", "foo");
editor.putString("nick", "bar");
//不要把復雜數(shù)據(jù)類型轉(zhuǎn)成String 存儲
editor.apply();
}
- 【推薦】SharedPreference 提交數(shù)據(jù)時霍弹, 盡量使用Editor#apply() 毫别, 而非
Editor#commit()。一般來講典格,僅當需要確定提交結(jié)果岛宦,并據(jù)此有后續(xù)操作時,才使
用Editor#commit()耍缴。
說明:
SharedPreference 相關修改使用apply 方法進行提交會先寫入內(nèi)存砾肺,然后異步寫入
磁盤挽霉, commit 方法是直接寫入磁盤。如果頻繁操作的話apply 的性能會優(yōu)于commit变汪,
apply 會將最后修改內(nèi)容寫入磁盤侠坎。但是如果希望立刻獲取存儲操作的結(jié)果,并據(jù)此
做相應的其他操作裙盾,應當使用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();
- 【強制】數(shù)據(jù)庫Cursor 必須確保使用完后關閉,以免內(nèi)存泄漏番官。
說明:
Cursor 是對數(shù)據(jù)庫查詢結(jié)果集管理的一個類庐完,當查詢的結(jié)果集較小時,消耗內(nèi)存不
易察覺徘熔。但是當結(jié)果集較大门躯,長時間重復操作會導致內(nèi)存消耗過大,需要開發(fā)者在
操作完成后手動關閉Cursor酷师。
數(shù)據(jù)庫Cursor 在創(chuàng)建及使用時生音,可能發(fā)生各種異常,無論程序是否正常結(jié)束窒升,必須
在最后確保Cursor 正確關閉缀遍,以避免內(nèi)存泄漏。同時饱须,如果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 不關閉
}
- 【強制】多線程操作寫入數(shù)據(jù)庫時蓉媳,需要使用事務譬挚,以免出現(xiàn)同步問題。
說明:
通過SQLiteOpenHelper 獲取數(shù)據(jù)庫SQLiteDatabase 實例酪呻,Helper 中會自動緩存已經(jīng)打開的SQLiteDatabase 實例减宣,單個App 中應使用SQLiteOpenHelper 的單例模式確保數(shù)據(jù)庫連接唯一。由于SQLite 自身是數(shù)據(jù)庫級鎖玩荠,單個數(shù)據(jù)庫操作是保證線程安全的(不能同時寫入)漆腌,transaction 是一次原子操作,因此處于事務中的操作是線程安全的阶冈。
若同時打開多個數(shù)據(jù)庫連接闷尿,并通過多線程寫入數(shù)據(jù)庫,會導致數(shù)據(jù)庫異常女坑,提示數(shù)據(jù)庫已被鎖住填具。
正例:
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);
}
- 【推薦】大數(shù)據(jù)寫入數(shù)據(jù)庫時,請使用事務或其他能夠提高I/O 效率的機制匆骗,保證執(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();
}
}
- 【強制】執(zhí)行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);
//請?zhí)岣甙踩庾R窿克,不要直接執(zhí)行字符串作為SQL 語句
db.execSQL(sqlStmt);
}
10.【強制】如果ContentProvider 管理的數(shù)據(jù)存儲在SQL 數(shù)據(jù)庫中,應該避免將不受
信任的外部數(shù)據(jù)直接拼接在原始SQL 語句中毛甲。
正例:
// 使用一個可替換參數(shù)
String mSelectionClause = "var = ?";
String[] selectionArgs = {""};
selectionArgs[0] = mUserInput;
反例:
// 拼接用戶輸入內(nèi)容和列名
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 等組件中使用圖片時,
應做好圖片的緩存蝗砾,避免始終持有圖片導致內(nèi)存溢出先较,也避免重復創(chuàng)建圖片,引起
性能問題悼粮。建議使用Fresco ( https://github.com/facebook/fresco )闲勺、Glide
(https://github.com/bumptech/glide)等圖片庫。
正例:
例如使用系統(tǒng)LruCache 緩存扣猫,參考:
https://developer.android.com/topic/performance/graphics/cache-bitmap.html
private LruCache<String, Bitmap> mMemoryCache;
@Override
protected void onCreate(Bundle savedInstanceState) {
...
// 獲取可用內(nèi)存的最大值菜循,使用內(nèi)存超出這個值將拋出OutOfMemory 異常。LruCache 通
過構(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> {
...
// 在后臺進行圖片解碼
@Override
protected Bitmap doInBackground(Integer... params) {
final Bitmap bitmap = decodeSampledBitmapFromResource(getResources(),
params[0], 100, 100));
addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
return bitmap;
}
...
}
反例:
沒有存儲,每次都需要解碼瀑凝,或者有緩存但是沒有合適的淘汰機制序芦,導致緩存效果
很差,依然經(jīng)常需要重新解碼粤咪。
【強制】png 圖片使用TinyPNG 或者類似工具壓縮處理,減少包體積渴杆。
【推薦】應根據(jù)實際展示需要寥枝,壓縮圖片宪塔,而不是直接顯示原圖。手機屏幕比較小囊拜,
直接顯示原圖某筐,并不會增加視覺上的收益,但是卻會耗費大量寶貴的內(nèi)存冠跷。
正例:
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);
// 然后根據(jù)圖片分辨率以及我們實際需要展示的大小南誊,計算壓縮率
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
// 設置壓縮率,并解碼
options.inJustDecodeBounds = false;
return BitmapFactory.decodeResource(res, resId, options);
}
反例:
不經(jīng)壓縮顯示原圖蜜托。
- 【強制】使用完畢的圖片抄囚,應該及時回收,釋放寶貴的內(nèi)存橄务。
正例:
Bitmap bitmap = null;
loadBitmapAsync(new OnResult(result){
bitmap = result;
});
...使用該bitmap...
// 使用結(jié)束幔托,在2.3.3 及以下需要調(diào)用recycle()函數(shù),在2.3.3 以上GC 會自動管理蜂挪,除非你明
確不需要再用重挑。
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.GINGERBREAD_MR1) {
bitmap.recycle();
}
bitmap = null;
反例:
使用完成圖片,始終不釋放資源棠涮。
- 【強制】在Activity#onPause()或Activity#onStop()回調(diào)中谬哀,關閉當前activity 正在執(zhí)
行的的動畫。
正例:
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()
}
}
反例:
頁面退出時史煎,不關閉該頁面相關的動畫。
- 【推薦】在動畫或者其他異步任務結(jié)束時诬垂,應該考慮回調(diào)時刻的環(huán)境是否還支持業(yè)
務處理劲室。例如Activity 的onStop()函數(shù)已經(jīng)執(zhí)行,且在該函數(shù)中主動釋放了資源结窘,
此時回調(diào)中如果不做判斷就會空指針崩潰很洋。
正例:
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);
}
}
反例:
動畫結(jié)束回調(diào)中,直接使用資源不加判斷隧枫,導致異常喉磁。
- 【推薦】使用 inBitmap 重復利用內(nè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)中運行协怒,嘗試使用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卑笨,在不怎么降低視覺效果的前提下孕暇,減少內(nèi)
存占用。
說明:
android.graphics.Bitmap.Config 類中關于圖片顏色的存儲方式定義:
- ALPHA_8 代表8 位Alpha 位圖;
- ARGB_4444 代表16 位ARGB 位圖妖滔;
- ARGB_8888 代表32 位ARGB 位圖隧哮;
- RGB_565 代表8 位RGB 位圖。
位圖位數(shù)越高座舍,存儲的顏色信息越多沮翔,圖像也就越逼真。大多數(shù)場景使用的是
ARGB_8888 和RGB_565曲秉,RGB_565 能夠在保證圖片質(zhì)量的情況下大大減少內(nèi)存
的開銷采蚀,是解決OOM 的一種方法。
但是一定要注意RGB_565 是沒有透明度的承二,如果圖片本身需要保留透明度榆鼠,那么
就不能使用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 結(jié)
合的形式構(gòu)建繪圖读虏。
11.【推薦】謹慎使用gif 圖片责静,注意限制每個頁面允許同時播放的gif 圖片,以及單個
gif 圖片的大小盖桥。
12.【參考】大圖片資源不要直接打包到apk灾螃,可以考慮通過文件倉庫遠程下載,減小包
體積揩徊。
13.【推薦】根據(jù)設備性能腰鬼,選擇性開啟復雜動畫,以實現(xiàn)一個整體較優(yōu)的性能和體驗塑荒;
14.【推薦】在有強依賴 onAnimationEnd 回調(diào)的交互時熄赡,如動畫播放完畢才能操作頁
面, onAnimationEnd 可能會因各種異常沒被回調(diào)( 參考:
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 執(zhí)行結(jié)束時,調(diào)用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);
安全
- 【強制】禁止使用常量初始化矢量參數(shù)構(gòu)建IvParameterSpec黄绩,建議IV 通過隨機方
式產(chǎn)生深员。
說明:
使用常量初始化向量尖昏,密碼文本的可預測性會高得多赖歌,容易受到字典式攻擊。iv 的
作用主要是用于產(chǎn)生密文的第一個block芜壁,以使最終生成的密文產(chǎn)生差異(明文相同
的情況下)礁凡,使密碼攻擊變得更為困難高氮。
正例:
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,阻止應用數(shù)據(jù)被導出把篓。
說明:
android:allowBackup 原本是 Android 提供的 adb 調(diào)試功能纫溃,如果設置為 true腰涧,
可以導出應用數(shù)據(jù)備份并在任意設備上恢復韧掩。這對應用安全性和用戶數(shù)據(jù)隱私構(gòu)成
極大威脅,所以必須設置為 false窖铡,防止數(shù)據(jù)泄露疗锐。
正例:
<application
android:allowBackup="false"
android:largeHeap="true"
android:icon="@drawable/test_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
- 【強制】如果使用自定義HostnameVerifier 實現(xiàn)類,必須在verify()方法中校驗服務
器主機名的合法性费彼,否則可能受到中間人攻擊滑臊。
說明:
在與服務器建立 https 連接時,如果 URL 的主機名和服務器的主機名不匹配箍铲,則
可通過該回調(diào)接口來判斷是否應該允許建立連接雇卷。如果回調(diào)內(nèi)實現(xiàn)不恰當,沒有有
效校驗主機名颠猴,甚至默認接受所有主機名关划,會大大增加安全風險。
反例:
HostnameVerifier hnv = new HostnameVerifier() {
@Override
public boolean verify(String hostname, SSLSession session) {
// 不做校驗翘瓮,接受任意域名服務器
return true;
}
};
HttpsURLConnection.setDefaultHostnameVerifier(hnv);
- 【強制】如果使用自定義X509TrustManager 實現(xiàn)類贮折,必須在checkServerTrusted()
方法中校驗服務端證書的合法性,否則可能受到中間人攻擊资盅。
說明:
常見誤區(qū)是checkServerTrusted()方法根本沒有實現(xiàn)调榄,這將導致 X509TrustManager
形同虛設。該方法中需要實現(xiàn)完備的校驗邏輯呵扛, 對于證書錯誤拋出
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 等)都不應在沒有嚴格權(quán)限控制的情況下利诺,將 android:exported 設置為 true。
【強制】WebView 應設置 WebView#getSettings()#setAllowFileAccess(false)剩燥、
WebView#getSettings()#setAllowFileAccessFromFileURLs(false) 慢逾、
WebView#getSettings()#setAllowUniversalAccessFromFileURLs(false)立倍,阻止 file
scheme URL 的訪問。
8.【強制】不要把敏感信息打印到log 中侣滩。
說明:
在開發(fā)過程中口注,為了方便調(diào)試,通常會使用log 函數(shù)輸出一些關鍵流程的信息君珠,這
些信息中通常會包含敏感內(nèi)容寝志,讓攻擊者更加容易了解APP 內(nèi)部結(jié)構(gòu),方便破解和
攻擊策添,甚至直接獲取到有價值的敏感信息材部。
反例:
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 打印程序的執(zhí)行過程的username 等調(diào)試信息,日志沒有
關閉唯竹,攻擊者可以直接從Logcat 中讀取這些敏感信息乐导。所以在產(chǎn)品的線上版本中關
閉調(diào)試接口,不要輸出敏感信息浸颓。
9.【強制】確保應用發(fā)布版本的android:debuggable 屬性設置為false物臂。
10.【強制】本地加密秘鑰不能硬編碼在代碼中,更不能使用 SharedPreferences 等本
地持久化機制存儲产上。應選擇Android 自身的秘鑰庫(KeyStore)機制或者其他安全
性更高的安全解決方案保存棵磷。
說明:
應用程序在加解密時,使用硬編碼在程序中的密鑰蒂秘,攻擊者通過反編譯拿到密鑰可
以輕易解密APP 通信數(shù)據(jù)泽本。
11.【建議】addJavascriptInterface() 可以添加JS 對本地Java 方法的調(diào)用,但這本身
會導致惡意代碼的攻擊姻僧。在Android 4.2(API Level 17)以下规丽,不應再使用這樣的
調(diào)用方式。在Android 4.2 及以上撇贺,需要對本地被遠程調(diào)用的方法顯式添加
@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 所關聯(lián)的 Window 應用 WindowManager.LayoutParams.FLAG_
SECURE错沃,防止被截屏、錄屏雀瓢。但要注意的是枢析,一個 Activity 關聯(lián)的 Window 可
能不止一個,如果使用了 Dialog / DialogFragment 等控件彈出對話框刃麸,它們本身
也會創(chuàng)建一個新的 Window醒叁,也一樣需要保護。
15.【推薦】zip 中不要包含 ../../file 這樣的路徑泊业,可能被篡改目錄結(jié)構(gòu)把沼,造成攻擊。
說明:
當zip 壓縮包中允許存在"../"的字符串吁伺,攻擊者可以利用多個"../"在解壓時改變zip 文
件存放的位置饮睬,當文件已經(jīng)存在是就會進行覆蓋,如果覆蓋掉的文件是so箱蝠、dex 或
者odex 文件续捂,就有可能造成嚴重的安全問題垦垂。
正例:
對路徑進行判斷,存在".."時拋出異常牙瓢。
//對重要的Zip 壓縮包文件進行數(shù)字簽名校驗劫拗,校驗通過才進行解壓
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 算法矾克,有一定的安全性页慷,
但不能代替加密算法。敏感信息的存儲和傳輸胁附,需要使用專業(yè)的加密機制酒繁。
其他
- 【強制】不能使用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 開發(fā)中弓候,有哪些坑需要注意郎哭? [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 數(shù)據(jù)庫模塊搭建方案 [EB/OL].
http://www.reibang.com/p/57eb08fe071d
---The end---