1. 程式人生 > >阿里巴巴Android開發手冊v1.0.1(網頁版)

阿里巴巴Android開發手冊v1.0.1(網頁版)

阿里巴巴Android開發手冊已經出了有段時間了,官方是pdf版的手冊,但我還是習慣閱讀網頁,於是我基於該手冊1.0.1版本整理了一個網頁版本的,習慣網頁閱讀的道友可以拿去欣賞了。

另外,文中會有一些我的個人備註,算是讀書筆記了,介意的可以忽略掉。
在這裡插入圖片描述

  • 【強制】 必須遵守,違反本約定或將會引起嚴重的後果;
  • 【推薦】 儘量遵守,長期遵守有助於系統穩定性和合作效率的提升;
  • 【參考】 充分理解,技術意識的引導,是個人學習、團隊溝通、專案合作的方向。
  • “說明”對內容做了適當擴充套件和解釋;“正例”提倡什麼樣的編碼和實現方式;“反例”說明需要提防的雷區,以及錯誤案例。"備註"這個是我自己加的,如有不同意見請忽略。

一、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 的item layout 以module_grid_item 開頭

備註:這裡我個人習慣還是以元件型別開頭: 元件型別_模組名_邏輯名, 例如activity_module_xxx, fragment_module_xxx, dialog_module_xxx,item_module_xxx等,感覺這樣找起來會更清晰明確一些,不過阿里這個也只是推薦的規則,你可以參考。

3. 【推薦】drawable 資源名稱以小寫單詞+下劃線的方式命名,根據解析度不同存放在不同的drawable 目錄下,如果介意包大小建議只使用一套,系統去進行縮放。

採用規則如下:

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

如:module_login_btn_pressed, module_tabs_icon_home_normal

備註:這裡我個人的習慣是圖示均以ic_開頭,背景圖以bg_開頭, ic_模組名_輯名_用途_狀態, 例如ic_user_share, ic_user_download, ic_login_btn_pressed, bg_user_guide, bg_user_blog等。

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

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

Tween 動畫(使用簡單影象變換的動畫,例如縮放、平移)資源:儘可能以通用的動畫名稱命名,如module_fade_in , module_fade_out , module_push_down_in (動畫+方向)。
Frame 動畫(按幀順序播放影象的動畫)資源:儘可能以模組+功能命名+序號。如module_loading_grey_001。

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

模組名_邏輯名稱_顏色

如:<color name="module_btn_bg_color">#33b5e5e5</color>

備註:通常我會引用@color/white, @color/black, @color/red等這樣的名字,但是有些color值實在不知道叫什麼名字了,就只能以color+顏色值的命名方式了。這樣的好處是能減少colors.xml中同一顏色值重複定義的次數,因為你再去嘗試定義這個顏色值時會發現它已經存在。如果是多處控制元件的引用相同顏色的話考慮到將來有可能修改,最好在style中作一層統一引用。

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

模組名_描述資訊

如:<dimen name="module_horizontal_line_height">1dp</dimen>

備註:跟color的命名一樣, 我個人習慣的做法是直接定義寬/高/間距/大小字尾+dp值字尾,如width_100_dp,height_45_dp, margin_15_dp, text_size_16_sp, 這樣的好處是已經定義過這個顏色值了你再去引用時就絕對不會重複,減少dimen.xml中的資源數量。如果是多個控制元件的引用相同的dp值的話最好在style中作一層統一引用。

7. 【推薦】style 資源採用 “父style 名稱.當前style 名稱” 方式命名,寫入module_styles.xml 檔案中,首字母大寫。如:

<style name="ParentTheme.ThisActivityTheme"></style>

8. 【推薦】string資原始檔或者文字用到字元需要全部寫入module_strings.xml 檔案中,字串以小寫單詞+下劃線的方式命名,採用以下規則:

模組名_邏輯名稱

如:moudule_login_tips,module_homepage_notice_desc

備註:個人習慣: 有明顯用途的,會加統一的字首, 例如用於提示/提醒用的會定義成remind_xxx或tips_xxx或toast_xxx, 用於標題的會定義成title_xxx, 用於輸入框hint值的會定義成hint_xxx等。

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

其它控制元件的縮寫推薦使用小寫字母並用下劃線進行分割,例如:ProgressBar 對應的縮寫為progress_bar;DatePicker 對應的縮寫為date_picker。

備註:id的命名一般習慣是:控制元件縮寫+下劃線+邏輯名,如 @+id/tv_user_name, 而程式碼中則會反過來定義,邏輯名+控制元件字尾,如 mUserNameText。

10.【推薦】圖片根據其解析度,放在不同螢幕密度的drawable 目錄下管理,否則可能在低密度裝置上導致記憶體佔用增加,又可能在高密度裝置上導致圖片顯示不夠清晰。

說明:
為了支援多種螢幕尺寸和密度,Android 提供了多種通用螢幕密度來適配。常用的如下。

ldpi - 120dpi
mdpi - 160dpi
hdpi - 240dpi
xhdpi - 320dpi
xxhdpi - 480dpi
xxxhdpi - 640dpi
Android 的螢幕解析度和密度並不存在嚴格的對應關係,應儘量避免直接基於解析度來開發,而是通過適配不同的螢幕密度來保證控制元件和圖片的顯示效果。不同密度drawable 目錄中的圖片解析度設定,參考不同密度的dpi 比例關係。

正例:

為顯示某個圖示,將48 x 48 的圖示檔案放在drawable-mdpi 目錄(160dpi)下;將72 x 72 的圖示檔案放在drawable-hdpi 目錄(240dpi)下;將96 x 96 的圖示檔案放在drawable-xhdpi 目錄(320dpi)下;將144 x 144 的圖示檔案放在drawable-xxhdpi 目錄(480dpi)下。

反例:

上述圖示,只有一個144 x 144 的圖示檔案放在drawable 目錄下。

備註:ldpi 的這個現在已經絕跡了,mdpi的也幾乎沒有了,現在新出的手機基本都在hdpi以上,然後記住一個比例,mdpi :hdpi:xhdpi:xxhdpi:xxxhdpi = 2:3:4:6:8,然後你就知道哪個資料夾下該放多大比例的圖片資源了。

三、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) {
		startActivity(intent);
	}else {
		// 找不到指定的Activity
	}
}

反例:

Intent intent = new Intent();
intent.setAction("com.example.DemoIntent ");
try {
	startActivity(intent);
} catch (ActivityNotFoundException e) {
	e.printStackTrace();
}

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) {
			}
		}
	}
}

備註:Service的生命週期都是執行在主執行緒中的。

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, 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
	}
};

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

7. 【推薦】新增Fragment 時, 確保FragmentTransaction#commit()Activity#onPostResume() 或者FragmentActivity#onResumeFragments() 內呼叫。不要隨意使用FragmentTransaction#commitAllowingStateLoss() 來代替,任何commitAllowingStateLoss() 的使用必須經過code review,確保無負面影響。

說明:

Activity 可能因為各種原因被銷燬, Android 支援頁面被銷燬前通過Activity#onSaveInstanceState() 儲存自己的狀態。但如果FragmentTransaction.commit() 發生在Activity 狀態儲存之後,就會導致Activity 重建、恢復狀態時無法還原頁面狀態,從而可能出錯。為了避免給使用者造成不好的體驗,系統會丟擲IllegalStateExceptionStateLoss 異常。推薦的做法是在ActivityonPostResume()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();
	}
}

備註:不太清楚這一條描述的是在什麼場景下有用,我也沒有遇到過這裡說明的問題,一般Fragment的使用要麼是在Activity建立onCreate的時候就添加了,要麼是後期點選某個按鈕新增/替換某個Fragment。

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

備註:我只在ViewPage當中的某一頁fragment中巢狀使用過包含fragment的ViewPage。

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

12.【推薦】對於只用於應用內的廣播,優先使用LocalBroadcastManager 來進行註冊和傳送,LocalBroadcastManager 安全性更好,同時擁有更高的執行效率。

說明:
對於使用Context#sendBroadcast() 等方法傳送全域性廣播的程式碼進行提示。如果該廣播僅用於應用內,則可以使用 LocalBroadcastManager 來避免廣播洩漏以及廣播被攔截等安全問題,同時相對全域性廣播本地廣播的更高效。

正例:

public class MainActivity extends ActionBarActivity {
	private MyReceiver receiver;
	private IntentFilter filter;
	private Context context;
	private static final String MY_BROADCAST_TAG = "com.example.localbroadcast";
	
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstsanceState);
		context = this;
		setContentView(R.layout.activity_main);
		receiver = new MyReceiver();
		filter = new IntentFilter();
		filter.addAction(MY_BROADCAST_TAG);
		Button button = (Button) findViewById(R.id.button);
		button.setOnClickListener(new View.OnClickListener() {
			@Override
			public void onClick(View view) {
				Intent intent = new Intent();
				intent.setAction(MY_BROADCAST_TAG);
				LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
			}
		});
	}
	
	@Override
	protected void onResume() {
		super.onResume();
		LocalBroadcastManager.getInstance(context).registerReceiver(receiver, filter);
	}

	@Override
	protected void onPause() {
		super.onPause();
		LocalBroadcastManager.getInstance(context).unregisterReceiver(receiver);
	}
	
	class MyReceiver extends BroadcastReceiver {
		@Override
		public void onReceive(Context arg0, Intent arg1) {
			// message received
		}
	}
	
}

反例:
所有廣播都使用全域性廣播

//In activity, sending broadcast
Intent intent = new Intent("com.example.broadcastreceiver.SOME_ACTION");
sendBroadcast(intent);

13.【推薦】當前ActivityonPause 方法執行結束後才會建立(onCreate)或恢復(onRestart)別的Activity,所以在onPause 方法中不適合做耗時較長的工作,這會影響到頁面之間的跳轉效率。

14. 【強制】Activity 或者Fragment 中動態註冊BroadCastReceiver 時,registerReceiver()unregisterReceiver() 要成對出現。

說明:
如果registerReceiver()unregisterReceiver() 不成對出現,則可能導致已經註冊的receiver 沒有在合適的時機登出,導致記憶體洩漏,佔用記憶體空間,加重SystemService負擔。部分華為的機型會對receiver 進行資源管控,單個應用註冊過多receiver 會觸發管控模組丟擲異常,應用直接崩潰。

正例:

pub