Android開發規范

一、Java語言規范

詳見:Android開發java編寫規范

二、Android資源文件命名與使用

1. 【推薦】資源文件需帶模塊前綴。

2. 【推薦】layout 文件的命名方式。

Activity 的 layout 以 module_activity 開頭

Fragment 的 layout 以 module_fragment 開頭

Dialog 的 layout 以 module_dialog 開頭

include 的 layout 以 module_include 開頭

ListView 的行 layout 以 module_list_item 開頭

RecyclerView 的 item layout 以 module_recycle_item 開頭

GridView 的行 layout 以 module_grid_item 開頭

3. 【推薦】 drawable 資源名稱以小寫單詞+下劃線的方式命名,根據分辨率不同存放 在不同的 drawable 目錄下,建議只使用一套,例如 drawable-xhdpi。采用規則如下:

模塊名_業務功能描述_控件描述_控件狀態限定詞

如:module_login_btn_pressed,module_tabs_icon_home_normal

4. 【推薦】anim 資源名稱以小寫單詞+下劃線的方式命名,采用以下規則:

模塊名_邏輯名稱_[方向|序號]

tween 動畫資源:盡可能以通用的動畫名稱命名,如 module_fade_in , module_fade_out , module_push_down_in (動畫+方向);

frame 動畫資源:盡可能以模 塊+功能命名+序號。如:module_loading_grey_001

5. 【推薦】 color 資源使用#AARRGGBB 格式,寫入 module_colors.xml 文件中,命 名格式采用以下規則:

模塊名_邏輯名稱_顏色

如:

#33b5e5e5

6. 【推薦】dimen 資源以小寫單詞+下劃線方式命名,寫入 module_dimens.xml 文件中, 采用以下規則:

模塊名_描述信息

如:

1dp

7. 【推薦】style 資源采用小寫單詞+下劃線方式命名,寫入 module_styles.xml 文件中, 采用以下規則:

父 style 名稱.當前 style 名稱

如:

8. 【推薦】 string資源文件或者文本用到字符需要全部寫入module_strings.xml文件中, 字符串以小寫單詞+下劃線的方式命名,采用以下規則:

模塊名_邏輯名稱

如:moudule_login_tips,module_homepage_notice_desc

9. 【推薦】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

二、Android資源文件命名與使用

其它控件的縮寫推薦使用小寫字母并用下劃線進行分割,例如:

ProgressBar 對應的縮寫為 progress_bar

DatePicker 對應的縮寫為 date_picker

10.【推薦】大分辨率圖片(單維度超過 1000)大分辨率圖片建議統一放在 xxhdpi 目錄 下管理,否則將導致占用內存成倍數增加。

說明:

為了支持多種屏幕尺寸和密度,Android 為多種屏幕提供不同的資源目錄進行適配。 為不同屏幕密度提供不同的位圖可繪制對象,可用于密度特定資源的配置限定符(在 下面詳述) 包括 ldpi(低)、mdpi(中)、 hdpi(高)、xhdpi(超高)、xxhdpi (超 超高)和 xxxhdpi(超超超高)。例如,高密度屏幕的位圖應使用 drawable-hdpi/。

根據當前的設備屏幕尺寸和密度,將會尋找最匹配的資源,如果將高分辨率圖片放 入低密度目錄,將會造成低端機加載過大圖片資源,又可能造成 OOM,同時也是資 源浪費,沒有必要在低端機使用大圖。

正例:

將 144*144 的應用圖標 PNG 文件放在 drawable-xxhdpi 目錄

反例:

將 144*144 的應用圖標 PNG 文件放在 drawable-mhdpi 目錄

擴展參考:

https://developer.android.com/guide/practices/screens_support.html?hl=zh-cn

三、Android基本組件

Android 基本組件指Activity、Fragment、Service、BroadcastReceiver、 ContentProvider 等等。

1. 【強制】Activity 間的數據通信,對于數據量比較大的,避免使用 Intent + Parcelable 的方式,可以考慮EventBus等替代方案,以免造成TransactionTooLargeException。

2. 【推薦】Activity#onSaveInstanceState()方法不是 Activity生命周期方法,也不保證 一定會被調用。它是用來在 Activity 被意外銷毀時保存 UI 狀態的,只能用于保存臨 時性數據,例如 UI 控件的屬性等,不能跟數據的持久化存儲混為一談。持久化存儲 應該在 Activity#onPause()/onStop()中實行。

3. 【強制】Activity間通過隱式Intent的跳轉,在發出Intent之前必須通過resolveActivity 檢查,避免找不到合適的調用組件,造成 ActivityNotFoundException 的異常。?

正例:

public void viewUrl(String url, String mimeType) {

Intent intent = new Intent(Intent.ACTION_VIEW);

intent.setDataAndType(Uri.parse(url), mimeType);

if (getPackageManager().resolveActivity(intent, PackageManager.MATCH_DEFAULT_ ONLY) != null) {

try {

startActivity(intent);

} catch (ActivityNotFoundException e) {

if (Config.LOGD) {

Log.d(LOGTAG, "activity not found for " + mimeType + " over " + Uri.parse(url). getScheme(), e);

}

}

}

}

三、Android基本組件?

反例:

Intent intent = new Intent();

intent.setAction("com.great.activity_intent.Intent_Demo1_Result3");

4. 【強制】避免在 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) {

}

}

}

}

5. 【強制】避免在 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, UseHomeActivity.class);

this.startActivity(userHomeIntent);

}

};

反例:

mBroadcastReceiver = new BroadcastReceiver() {

@Override

public void onReceive(Context context, Intent intent) {

MyDatabaseHelper myDB = new MyDatabaseHelper(context);

myDB.initData();

// have more database operation here

}

};

擴展參考:

https://developer.android.com/reference/android/content/BroadcastReceiver.html#onReceive(android.content.Context, android.content.Intent)

6. 【強制】避免使用隱式 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);

}

}

}

}

擴展參考:

1)https://wiki.sei.cmu.edu/confluence/display/android/DRD03-J.+Do+not+broadcast+sensitive+information+using+an+implicit+intent

2)https://cwe.mitre.org/data/definitions/927.html

7. 【推薦】添加 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();

}

}

擴展參考:

1)https://www.androiddesignpatterns.com/2013/08/fragment-transaction-commit-state-loss.html

2)https://developer.android.com/reference/android/app/FragmentTransaction.html#commit())

8. 【推薦】不要在 Activity#onDestroy()內執行釋放資源的工作,例如一些工作線程的 銷毀和停止,因為 onDestroy()執行的時機可能較晚。可根據實際需要,在 Activity#onPause()/onStop()中結合 isFinishing()的判斷來執行。

9. 【推薦】如非必須,避免使用嵌套的 Fragment。

說明:

嵌套 Fragment 是在 Android API 17 添加到 SDK 以及 Support 庫中的功能, Fragment 嵌套使用會有一些坑,容易出現 bug,比較常見的問題有如下幾種:

1) onActivityResult()方法的處理錯亂,內嵌的 Fragment 可能收不到該方法的回調, 需要由宿主 Fragment 進行轉發處理;

2) 突變動畫效果;

3) 被繼承的 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();

擴展參考:

1)https://inthecheesefactory.com/blog/onactivityresult-nested-fragment-supportlibrary-v23.2/en

2)http://blog.csdn.net/megatronkings/article/details/51417510

10.【推薦】總是使用顯式 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();

...

}

}

擴展參考:

https://developer.android.com/training/run-background-service/index.html

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(savedInstanceState);

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);

13.【推薦】當前Activity的onPause方法執行結束后才會執行下一個Activity的onCreate 方法,所以在 onPause 方法中不適合做耗時較長的工作,這會影響到頁面之間的跳 轉效率。

14.【強制】不要在 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;

void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.get_username);

tv = (TextView)findViewById(R.id.username);

}

void onResume() {

super.onResume();

MyApplication app = (MyApplication) getApplication();

tv.setText("Welcome back ! " + app.getUsername().toUpperCase());

}

}

15.【推薦】使用 Toast 時,建議定義一個全局的 Toast 對象,這樣可以避免連續顯示 Toast 時不能取消上一次 Toast 消息的情況(如果你有連續彈出 Toast 的情況,避免 使用 Toast.makeText)。

16.【強制】使用 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;

}

17.【強制】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 產生內存泄漏。

四、UI與布局

1. 【強制】布局中不得不使用 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 里的 Hierarachy Viewer 工具,可視化的查看所有的 view。

正例:


...

反例:


...

多重嵌套導致 measure 以及 layout 等步驟耗時過多。

擴展參考:

1)https://developer.android.com/studio/profile/hierarchy-viewer.html

2)http://mrpeak.cn/android/2016/01/11/android-performance-ui

3)https://www.safaribooksonline.com/library/view/high-performance-android/9781491913994/ch04.html#figure-story_tree

2. 【推薦】在 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);

}

3. 【推薦】源文件統一采用 UTF-8 的形式進行編碼。

4. 【強制】禁止在非 ui 線程進行 view相關操作。

5. 【推薦】文本大小使用單位 dp,view 大小使用單位 dp。對于 Textview,如果在文 字大小確定的情況下推薦使用 wrap_content 布局避免出現文字顯示不全的適配問 題。

6. 【強制】禁止在設計布局時多次設置子 view 和父 view 中為同樣的背景造成頁面過 度繪制,推薦將不需要顯示的布局進行及時隱藏。

正例:


http://schemas.android.com/apk/res/android"

android:layout_width="fill_parent"

android:layout_height="fill_parent"

android:orientation="vertical" >

android:layout_width="fill_parent"

android:layout_height="wrap_content"

android:text="@string/hello" />

android:layout_width="fill_parent"

android:layout_height="wrap_content"

android:text="click it !"

android:id="@+id/btn_mybuttom" />

android:id="@+id/img"

android:layout_width="fill_parent"

android:layout_height="wrap_content"

android:visibility="gone"

android:src="@drawable/youtube" />

android:text="it is an example!"

android:layout_width="fill_parent"

android:layout_height="wrap_content" />

反例:

@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);

}

7. 【推薦】靈活使用布局,推薦 Merge、ViewStub 來優化布局,盡可能多的減少 UI 布局層級,推薦使用 FrameLayout,LinearLayout、RelativeLayout 次之。

8. 【推薦】在需要時刻刷新某一區域的組件時,建議通過以下方式避免引發全局 layout 刷新:

1) 設置固定的 view大小的高寬,如倒計時組件等;

2) 調用 view的 layout 方式修改位置,如彈幕組件等;

3) 通過修改 canvas 位置并且調用 invalidate(int l, int t, int r, int b)等方式限定刷新 區域;

4) 通過設置一個是否允許 requestLayout 的變量,然后重寫控件的 requestlayout、 onSizeChanged 方法,判斷控件的大小沒有改變的情況下,當進入 requestLayout 的時候,直接返回而不調用 super 的 requestLayout 方法。

9. 【推薦】不能在 Activity沒有完全顯示時顯示 PopupWindow 和 Dialog。

10.【推薦】盡量不要使用 AnimationDrawable,它在初始化的時候就將所有圖片加載 到內存中,特別占內存,并且還不能釋放,釋放之后下次進入再次加載時會報錯。

說明:

Android 的幀動畫可以使用 AnimationDrawable 實現,但是如果你的幀動畫中如果 包含過多幀圖片,一次性加載所有幀圖片所導致的內存消耗會使低端機發生 OOM 異常。幀動畫所使用的圖片要注意降低內存消耗,當圖片比較大時,容易出現 OOM。

正例:

圖片數量較少的 AnimationDrawable 還是可以接受的。


http://schemas.android.com/apk/res/android" android:oneshot ="true">

反例:

http://schemas.android.com/apk/res/android" android:oneshot ="false">

上述如此多圖片的動畫就不建議使用 AnimationDrawable 了。

擴展參考:

1)https://stackoverflow.com/questions/8692328/causing-outofmemoryerror-in-frame-by-frame-animation-in-android

2)http://blog.csdn.net/wanmeilang123/article/details/53929484

3)https://segmentfault.com/a/1190000005987659

4)https://developer.android.com/reference/android/graphics/drawable/AnimationDrawable.html

11.【強制】不能使用 ScrollView 包裹 ListView/GridView/ExpandableListVIew;因為這 樣會把 ListView 的所有 Item 都加載到內存中,要消耗巨大的內存和 cpu 去繪制圖 面。

說明:

ScrollView中嵌套 List 或 RecyclerView的做法官方明確禁止。除了開發過程中遇到 的各種視覺和交互問題,這種做法對性能也有較大損耗。ListView 等 UI 組件自身有 垂直滾動功能,也沒有必要在嵌套一層 ScrollView。目前為了較好的 UI 體驗,更貼 近 Material Design 的設計,推薦使用 NestedScrollView。

正例:


...

反例:

...

擴展參考:

1)https://developer.android.com/reference/android/widget/ScrollView.html

2)https://developer.android.com/reference/android/support/v4/widget/NestedScrollView.html

五、進程、線程與消息通信

1. 【強制】不要通過 Intent 在 Android 基礎組件之間傳遞大數據(binder transaction 緩存為 1MB),可能導致 OOM。

2. 【強制】在 Application 的業務初始化代碼加入進程判斷,確保只在自己需要的進程 初始化。特別是后臺進程減少不必要的業務初始化。

正例:

public class MyApplication extends Application {

@Override

public void onCreate() {

//在所有進程中初始化

....

//僅在主進程中初始化

if (mainProcess) {

...

}

//僅在后臺進程中初始化

if (bgProcess) {

...

}

}

}

3. 【強制】新建線程時,必須通過線程池提供(AsyncTask 或者 ThreadPoolExecutor 或者其他形式自定義的線程池),不允許在應用中自行顯式創建線程。

說明:

使用線程池的好處是減少在創建和銷毀線程上所花的時間以及系統資源的開銷,解 決資源不足的問題。如果不使用線程池,有可能造成系統創建大量同類線程而導致

消耗完內存或者“過度切換”的問題。另外創建匿名線程不便于后續的資源使用分析, 對性能分析等會造成困擾。

正例:

int NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors();

int KEEP_ALIVE_TIME = 1;

TimeUnit KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS;

BlockingQueue taskQueue = new LinkedBlockingQueue();

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();

擴展參考:

https://blog.mindorks.com/threadpoolexecutor-in-android-8e9d22330ee3

4. 【強制】線程池不允許使用 Executors 去創建,而是通過 ThreadPoolExecutor 的方 式,這樣的處理方式讓寫的同學更加明確線程池的運行規則,規避資源耗盡的風險。

說明:

Executors 返回的線程池對象的弊端如下:

1) FixedThreadPool 和 SingleThreadPool : 允 許 的 請 求 隊 列 長 度 為 Integer.MAX_VALUE,可能會堆積大量的請求,從而導致 OOM;

2) 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 taskQueue = new LinkedBlockingQueue();

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();

擴展參考:

http://dev.bizo.com/2014/06/cached-thread-pool-considered-harmlful.html

5. 【強制】子線程中不能更新界面,更新界面必須在主線程中進行,網絡操作不能在 主線程中調用。

6. 【強制】不要在非UI 線程中初始化 ViewStub,否則會返回null。

7. 【推薦】盡量減少不同 APP 之間的進程間通信及拉起行為。拉起導致占用系統資源, 影響用戶體驗。

8. 【推薦】新建線程時,定義能識別自己業務的線程名稱,便于性能優化和問題排查。

正例:

public class MyThread extends Thread {

public MyThread(){

super.setName("ThreadName");

}

}

9. 【推薦】ThreadPoolExecutor 設置線程存活時間(setKeepAliveTime),確保空閑時 線程能被釋放。

10.【推薦】禁止在多進程之間用 SharedPreferences 共享數據,雖然可以 (MODE_MULTI_PROCESS),但官方已不推薦。

11.【推薦】謹慎使用 Android 的多進程,多進程雖然能夠降低主進程的內存壓力,但會 遇到如下問題:

1) 不能實現完全退出所有 Activity的功能;

2) 首次進入新啟動進程的頁面時會有延時的現象(有可能黑屏、白屏幾秒,是白 屏還是黑屏和新 Activity的主題有關);

3) 應用內多進程時,Application 實例化多次,需要考慮各個模塊是否都需要在所 有進程中初始化;

4) 多進程間通過 SharedPreferences 共享數據時不穩定。

六、文件與數據庫

1. 【強制】任何時候不要硬編碼文件路徑,請使用 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;

}

擴展參考:

1)https://developer.android.com/training/data-storage/files.html

2)https://developer.android.com/reference/android/os/Environment.html#getExternalStorageDirectory()

2. 【強制】當使用外部存儲時,必須檢查外部存儲的可用性。

正例:

// 讀/寫檢查

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.equals(state) ||

Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)) {

return true;

}

return false;

}

3. 【強制】應用間共享文件時,不要通過放寬文件系統權限的方式去實現,而應使用 FileProvider。

正例:


...

...

android:name="android.support.v4.content.FileProvider"

android:authorities="com.example.fileprovider"

android:exported="false"

android:grantUriPermissions="true">

android:name="android.support.FILE_PROVIDER_PATHS"

android:resource="@xml/provider_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);

}

4. 【推薦】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();

}

5. 【推薦】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();

擴展參考:

https://developer.android.com/reference/android/content/SharedPreferences.Editor.html#apply()

6. 【強制】數據庫 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 不關閉

}

7. 【強制】多線程操作寫入數據庫時,需要使用事務,以免出現同步問題。

說明:

Android 的通過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);

}

擴展參考:

1)https://nfrolov.wordpress.com/2014/08/16/android-sqlitedatabase-locking-and-multi-threading/

2)https://developer.android.com/reference/android/database/sqlite/SQLiteDatabase.html#beginTransaction())

3)https://www.androiddesignpatterns.com/2012/05/correctly-managing-your-sqlite-database.html

4)http://www.lxweimin.com/p/57eb08fe071d

8. 【推薦】大數據寫入數據庫時,請使用事務或其他能夠提高 I/O 效率的機制,保證執 行速度。

正例:

public void insertBulk(SQLiteDatabase db, ArrayList 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. 【強制】執行 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 語句中,可使用一個用于將 ? 作為可替換參 數的選擇子句以及一個單獨的選擇參數數組,會避免 SQL 注入。

正例:

// 使用一個可替換參數

String mSelectionClause = "var = ?";

String[] selectionArgs = {""};

selectionArgs[0] = mUserInput;

反例:

// 拼接用戶輸入內容和列名

String mSelectionClause = "var = " + mUserInput;

七、Bitmap、Drawable與動畫

1. 【強制】加載大圖片或者一次性加載多張圖片,應該在異步線程中進行。圖片的加 載,涉及到 IO 操作,以及 CPU 密集操作,很可能引起卡頓。

正例:

class BitmapWorkerTask extends AsyncTask {

...

// 在后臺進行圖片解碼

@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");

}

});

2. 【強制】在 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 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(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 {

...

// 在后臺進行圖片解碼

@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 或者類似工具壓縮處理,減少包體積。

4. 【推薦】應根據實際展示需要,壓縮圖片,而不是直接顯示原圖。手機屏幕比較小, 直接顯示原圖,并不會增加視覺上的收益,但是卻會耗費大量寶貴的內存。

正例:

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);

}

反例:

不經壓縮顯示原圖。

5. 【強制】使用完畢的圖片,應該及時回收,釋放寶貴的內存。

正例:

Bitmap bitmap = null;

loadBitmapAsync(new OnResult(result){

bitmap = result;

});

...使用該 bitmap...

// 使用結束,在 2.3.3 及以下需要調用 recycle()函數,在 2.3.3 以上 GC 會自動管理,除非你明 確不需要再用。

if (Build.VERSION.SDK_INT <= 10) {

bitmap.recycle();

}

bitmap = null;

反例:

使用完成圖片,始終不釋放資源。

6. 【推薦】針對不同的屏幕密度,提供對應的圖片資源,使內存占用和顯示效果達到 合理的平衡。如果為了節省包體積,可以在不影響 UI 效果的前提下,省略低密度圖 片。

7. 【強制】在 Activity.onPause()或 Activity.onStop()回調中,關閉當前 activity 正在執 行的的動畫。

正例

public class MyActivity extends Activity {

ImageView mImageView;

Animation mAnimation;

Button mBtn;

/** 首次創建 activity 時調用 */

@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);

}

});

}

public void onPause() {

//頁面退出,及時清理動畫資源

mImageView.clearAnimation()

}

}

反例:

頁面退出時,不關閉該頁面相關的動畫。

8. 【推薦】在動畫或者其他異步任務結束時,應該考慮回調時刻的環境是否還支持業 務處理。例如 Activity 的 onStop()函數已經執行,且在該函數中主動釋放了資源, 此時回調中如果不做判斷就會空指針崩潰。

正例:

public class MyActivity extends Activity {

private ImageView mImageView;

private Animation mAnimation;

/** 首次創建 activity 時調用 */

@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);

}

}

反例:

動畫結束回調中,直接使用資源不加判斷,導致異常。

9. 【推薦】使用 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;

}

}

}

10.【推薦】使用 ARGB_565 代替 ARGB_888,在不怎么降低視覺效果的前提下,減少 內存占用。

說明:

android.graphics.Bitmap.Config 類中關于圖片顏色的存儲方式定義:

1) ALPHA_8 代表 8 位 Alpha 位圖;

2) ARGB_4444 代表16位 ARGB 位圖;

3) ARGB_8888 代表32位 ARGB 位圖;

4) RGB_565 代表 8 位 RGB 位圖。

位圖位數越高,存儲的顏色信息越多,圖像也就越逼真。大多數場景使用的是 ARGB_8888 和 RGB_565,RGB_565 能夠在保證圖片質量的情況下大大減少內存 的開銷,是解決 oom 的一種方法。

但是一定要注意 RGB_565 是沒有透明度的,如果圖片本身需要保留透明度,那么 就不能使用 RGB_565。

正例:

Config config = drawableSave.getOpacity() != PixelFormat.OPAQUE ? Config.ARGB_8888 : Config.RGB_565;

Bitmap bitmap = Bitmap.createBitmap(w, h, config);

反例:

Bitmap newb = Bitmap.createBitmap(width, height, Config.ARGB_8888);

擴展參考:

1)http://www.lxweimin.com/p/294b390eb6f7

2)https://www.programcreek.com/java-api-examples/android.graphics.Bitmap.Config

11.【推薦】盡量減少 Bitmap(BitmapDrawable)的使用,盡量使用純色(ColorDrawable)、 漸變色(GradientDrawable)、 StateSelector(StateListDrawable)等與 Shape 結 合的形式構建繪圖。

12.【推薦】謹慎使用 gif 圖片,注意限制每個頁面允許同時播放的 gif 圖片,以及單個 gif 圖片的大小。

13.【參考】大圖片資源不要直接打包到 apk,可以考慮通過文件倉庫遠程下載,減小包 體積。

14.【推薦】根據設備性能,選擇性開啟復雜動畫,以實現一個整體較優的性能和體驗;

15.【推薦】在有強依賴 onAnimationEnd 回調的交互時,如動畫播放完畢才能操作頁 面,onAnimationEnd 可 能 會 因 各 種 異 常 沒 被 回 調 ( 參 考 :https://stackoverflow.com/questions/5474923/onanimationend-is-not-getting-called-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);

16.【推薦】當 View Animation 執行結束時,調用 View.clearAnimation()釋放相關資源。

正例:

View v = findViewById(R.id.xxxViewID);

final FadeUpAnimation anim = new FadeUpAnimation(v);

anim.setInterpolator(new AccelerateInterpolator());

anim.setDuration(1000);

anim.setFillAfter(true);

anim.setAnimationListener(new AnimationListener() {

@Override

public void onAnimationEnd(Animation arg0) {

//判斷一下資源是否被釋放了

if (v != null) {

v.clearAnimation();

}

}

});

v.startAnimation(anim);

八、安全

1. 【強制】使用 PendingIntent 時,禁止使用空 intent,同時禁止使用隱式 Intent

說明:

1) 使用 PendingIntent 時,使用了空 Intent,會導致惡意用戶劫持修改 Intent 的內 容。禁止使用一個空 Intent 去構造 PendingIntent,構造 PendingIntent 的 Intent 一定要設置 ComponentName 或者action。

2) PendingIntent 可以讓其他 APP 中的代碼像是運行自己 APP 中。PendingIntent 的intent接收方在使用該intent時與發送方有相同的權限。在使用PendingIntent 時,PendingIntent 中包裝的 intent 如果是隱式的 Intent,容易遭到劫持,導致 信息泄露。

正例:

Intent intent = new Intent(this, SomeActivity.class);

PendingIntent pendingIntent = PendingIntent.getActivity(this, 1, intent, PendingIntent.FLAG_ UPDATE_CURRENT);

try {

pendingIntent.send();

} catch (PendingIntent.CanceledException e) {

e.printStackTrace();

}

反例 1:

Bundle addAccountOptions = new Bundle();

mPendingIntent = PendingTntent.getBroadcast(this, 0, new Intent, 0);

addAccountOptions.putParcelable(KEY_CALLER_IDENTITY, mPendingIntent);

addAccountOptions.putBoolean(EXTRA_HAS_MULTIPLE_USERS, Utils.hasMultipleUsers(this));

AccountManager.get(this).addAccount(?accountType,?null,?null,?addAccountOptions,?null,?mCallback,?null);

反例 2:

mPendingIntent 是通過 new Intent()構造原始 Intent 的,所以為“雙無”Intent,這個 PendingIntent最終被通過AccountManager.addAccount方法傳遞給了惡意APP接 口。

Intent intent = new Intent("com.test.test.pushservice.action.METHOD");

intent.addFlags(32);

intent.putExtra("app",

PendingIntent.getBroadcast(this, 0, intent, 0));

如上代碼PendingIntent.getBroadcast,PendingItent中包含的Intent為隱式intent, 因此當PendingIntent觸發執行時,發送的intent很可能被嗅探或者劫持,導致intent 內容泄漏。

擴展參考:

1)https://developer.android.com/reference/android/app/PendingIntent.html

2)https://wiki.sei.cmu.edu/confluence/display/android/DRD21-J.+Always+pass+explicit+intents+to+a+PendingIntent

3)http://www.droidsec.cn/android-broadcastanywhere%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/

2. 【強制】禁止使用常量初始化矢量參數構建 IvParameterSpec,建議 IV 通過隨機方 式產生。

說明:

使用固定初始化向量,結果密碼文本可預測性會高得多,容易受到字典式攻擊。iv 的作用主要是用于產生密文的第一個 block,以使最終生成的密文產生差異(明文相 同的情況下),使密碼攻擊變得更為困難,除此之外 iv 并無其它用途。因此 iv 通過

隨機方式產生是一種十分簡便、有效的途徑。

正例:

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());

3. 【強制】將 android:allowbackup 屬性設置為 false,防止 adb backup 導出數據。

說明:

在 AndroidManifest.xml 文件中為了方便對程序數據的備份和恢復在 Android API level 8 以后增加了 android:allowBackup 屬性值。默認情況下這個屬性值為 true,故 當 allowBackup 標志值為 true 時,即可通過 adb backup 和adb restore 來備份和恢 復應用程序數據。

正例:

android:allowBackup="false"

android:largeHeap="true"

android:icon="@drawable/test_launcher"

android:label="@string/app_name"

android:theme="@style/AppTheme" >

4. 【強制】在實現的 HostnameVerifier 子類中,需要使用 verify 函數效驗服務器主機 名的合法性,否則會導致惡意程序利用中間人攻擊繞過主機名效驗。

說明:

在握手期間,如果 URL 的主機名和服務器的標識主機名不匹配,則驗證機制可以 回調此接口的實現程序來確定是否應該允許此連接。如果回調內實現不恰當,默認

接受所有域名,則有安全風險。

反例:

HostnameVerifier hnv = new HostnameVerifier() {

@Override

public boolean verify(String hostname, SSLSession session) {

// 總是返回 true,接受任意域名服務器

return true;

}

};

HttpsURLConnection.setDefaultHostnameVerifier(hnv);

正例:

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. 【強制】利用 X509TrustManager 子類中的 checkServerTrusted 函數效驗服務器端 證書的合法性。

說明:

在實現的 X509TrustManager 子類中未對服務端的證書做檢驗,這樣會導致不被信

任的證書繞過證書效驗機制。

反例:

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);

6. 【強制】META-INF 目錄中不能包含如.apk,.odex,.so 等敏感文件,該文件夾沒有經 過簽名,容易被惡意替換。

7. 【強制】Receiver/Provider 不能在毫無權限控制的情況下,將 android:export 設置 為 true。

8. 【參考】數據存儲在 Sqlite 或者輕量級存儲需要對數據進行加密,取出來的時候進 行解密。

9. 【強制】阻止 webview 通過 file:schema 方式訪問本地敏感數據。

10.【強制】不要廣播敏感信息,只能在本應用使用 LocalBroadcast,避免被別的應用 收到,或者 setPackage 做限制。

11.【強制】不要把敏感信息打印到 log 中。

說明:

在 APP 的開發過程中,為了方便調試,通常會使用 log 函數輸出一些關鍵流程的信 息,這些信息中通常會包含敏感內容,如執行流程、明文的用戶名密碼等,這會讓 攻擊者更加容易的了解 APP 內部結構方便破解和攻擊,甚至直接獲取到有價值的敏 感信息。

反例:

String username = "log_leak";

String password = "log_leak_pwd";

Log.d("MY_APP", "usesname" + username);

Log.d("MY_APP", "password" + password, new Throwable());

Log.v("MY_APP", "send message to server ");

以上代碼使用 Log.d Log.v 打印程序的執行過程的 username 等調試信息,日志沒 有關閉,攻擊者可以直接從 Logcat 中讀取這些敏感信息。所以在產品的線上版本中 關閉調試接口,不要輸出敏感信息。

12.【強制】對于內部使用的組件,顯示設置組件的"android:exported"屬性為false。

說明:

Android 應用使用 Intent 機制在組件之間傳遞數據,如果應用在使用 getIntent(), getAction(),Intent.getXXXExtra()獲取到空數據、異常或者畸形數據時沒有進行異 常捕獲,應用就會發生 Crash,應用不可使用(本地拒絕服務)。惡意應用可通過向 受害者應用發送此類空數據、異常或者畸形數據從而使應用產生本地拒絕服務。

13.【強制】應用發布前確保 android:debuggable 屬性設置為false。

14.【強制】使用 Intent Scheme URL 需要做過濾。

說明:

如果瀏覽器支持 Intent Scheme Uri 語法,如果過濾不當,那么惡意用戶可能通過瀏 覽器 js 代碼進行一些惡意行為,比如盜取 cookie 等。如果使用了 Intent.parseUri 函 數 , 獲 取 的 intent 必須嚴格過濾,intent 至少包含 addCategory(“android.intent.category.BROWSABLE”) , setComponent(null) , setSelector(null)3 個策略。

正例:

// 將 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);

擴展參考:

1)https://jaq.alibaba.com/community/art/show?articleid=265

2)https://www.mbsd.jp/Whitepaper/IntentScheme.pdf

15.【強制】密鑰加密存儲或者經過變形處理后用于加解密運算,切勿硬編碼到代碼中。

說明:

應用程序在加解密時,使用硬編碼在程序中的密鑰,攻擊者通過反編譯拿到密鑰可 以輕易解密 APP 通信數據。

16.【強制】將所需要動態加載的文件放置在 apk 內部,或應用私有目錄中,如果應用 必須要把所加載的文件放置在可被其他應用讀寫的目錄中(比如 sdcard),建議對不 可信的加載源進行完整性校驗和白名單處理,以保證不被惡意代碼注入。

17.【強制】除非min API level >=17,請注意 addJavascriptInterface 的使用。

說明:

API level>=17,允許 js 被調用的函數必須以@JavascriptInterface 進行注解,因此 不受影響; 對于 API level < 17,盡量不要使用 addJavascriptInterface,如果一定 要用,那么:

1) 使用 https 協議加載 URL,使用證書校驗,防止訪問的頁面被篡改掛馬;

2) 對加載 URL 做白名單過濾、完整性校驗等防止訪問的頁面被篡改;

3) 如果加載本地 html,應該會 HTML 內置在 APK 中,以及對 HTML 頁面進行完整 性校驗。

18.【強制】使用 Android 的 AES/DES/DESede 加密算法時,不要使用默認的加密模式 ECB,應顯示指定使用 CBC 或 CFB 加密模式。

說明:

加密模式 ECB、CBC、CFB、OFB 等,其中 ECB 的安全性較弱,會使相同的銘文 在不同的時候產生相同的密文,容易遇到字典攻擊,建議使用 CBC 或 CFB 模式。

1) ECB:Electronic codebook,電子密碼本模式

2) CBC:Cipher-block chaining,密碼分組鏈接模式

3) CFB:Cipher feedback,密文反饋模式

4) OFB:Output feedback,輸出反饋模式

19.【強制】不要使用 loopback 來通信敏感信息。

20.【推薦】對于不需要使用 File 協議的應用,禁用 File 協議,顯式設置 webView. getSettings().setAllowFileAccess(false),對于需要使用 File 協議的應用,禁止 File 協議調用JavaScript,顯式設置webView.getSettings().setJavaScriptEnabled(false)。

21.【強制】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。

22.【推薦】Android5.0 以后安全性要求較高的應用應該使用 window.setFlag (LayoutParam.FLAG_SECURE) 禁止錄屏。

23.【推薦】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();

}

}

如上代碼,沒有對文件的路徑名進行判斷直接進行解壓,如果路徑中包含../字符串, 就會造成目錄的遍歷問題,一旦遭到中間人攻擊替換下載的文件,將會導致某些惡 意文件被執行。

24.【強制】開放的 activity/service/receiver 等需要對傳入的 intent 做合法性校驗。

25.【推薦】加密算法:使用不安全的 Hash 算法(MD5/SHA-1)加密信息,存在被破解 的風險,建議使用 SHA-256 等安全性更高的Hash 算法。

26.【推薦】Android WebView 組件加載網頁發生證書認證錯誤時,采用默認的處理方法 handler.cancel(),停止加載問題頁面。

說明:

Android WebView 組件加載網頁發生證書認證錯誤時,會調用WebViewClient 類的 onReceivedSslError 方法,如果該方法實現調用了 handler.proceed()來忽略該證書 錯誤,則會受到中間人攻擊的威脅,可能導致隱私泄露.

反例:

mWebView.getSettings().setJavaScriptEnabled(true);

mWebView.addJavascriptInterface(new JsBridge(mContext), JS_OBJECT);

mWebView.loadUrl("http://www.example.org/tests/addjsif/");

mWebView.setWebViewClient(new WebViewClient() {

@Override

public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {

handler.proceed(); // 忽略 SSL 證書錯誤

}

});

27.【推薦】直接傳遞命令字或者間接處理有敏感信息或操作時,避免使用 socket 實現, 使用能夠控制權限校驗身份的方式通訊。

九、其他

1. 【強制】不要通過Msg 傳遞大的對象,會導致內存問題。

2. 【強制】不能使用System.out.println 打印 log。

正例:

Log.d(TAG, "Some Android Debug info ...");

反例:

System.out.println("System out println ...");

3. 【強制】Log的 tag不能是" "。

說明:

日志的 tag 是空字符串沒有任何意義,也不利于過濾日志。

正例:

private static String TAG = "LoginActivity";

Log.e(TAG, "Login failed!");

反例:

Log.e("", "Login failed!");

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容