阿里Android開發規範:四大基本元件

leeyh發表於2018-03-06

以下內容摘自 阿里巴巴Android開發手冊

我們的目標是:

  • 防患未然,提升質量意識,降低故障率和維護成本;
  • 標準統一,提升協作效率;
  • 追求卓越的工匠精神,打磨精品程式碼。
  • 【強制】必須遵守,違反本約定或將會引起嚴重的後果;
  • 【推薦】儘量遵守,長期遵守有助於系統穩定性和合作效率的提升;
  • 【參考】充分理解,技術意識的引導,是個人學習、團隊溝通、專案合作的方向。

阿里Android開發規範:資原始檔命名與使用規範
阿里Android開發規範:四大基本元件
阿里Android開發規範:UI 與佈局
阿里Android開發規範:程式、執行緒與訊息通訊
阿里Android開發規範:檔案與資料庫
阿里Android開發規範:Bitmap、Drawable 與動畫
阿里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);
			}
		}
	}
}
複製程式碼

反例:

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
	}
};
複製程式碼

擴充套件參考: developer.android.com/reference/a…
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();
intent.setAction("com.sample.action.server_running");
intent.putExtra("local_ip", v0.h);
intent.putExtra("port", v0.i);
intent.putExtra("code", v0.g);
intent.putExtra("connected", v0.s);
intent.putExtra("pwd_predefined", v0.r);
if (!TextUtils.isEmpty(v0.t)) {
	intent.putExtra("connected_usr", v0.t);
}
context.sendBroadcast(intent);
複製程式碼

以上廣播可能被其他應用的如下 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. wiki.sei.cmu.edu/confluence/…
  2. cwe.mitre.org/data/defini…

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. inthecheesefactory.com/blog/onacti…
  2. blog.csdn.net/megatronkin…

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();
	...
	}
}
複製程式碼

擴充套件參考: developer.android.com/training/ru…
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 產生記憶體洩漏。

阿里Android開發規範:資原始檔命名與使用規範
阿里Android開發規範:四大基本元件
阿里Android開發規範:UI 與佈局
阿里Android開發規範:程式、執行緒與訊息通訊
阿里Android開發規範:檔案與資料庫
阿里Android開發規範:Bitmap、Drawable 與動畫
阿里Android開發規範:安全與其他

相關文章