1. 程式人生 > >阿里Android開發規範:四大基本元件

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

以下內容摘自 阿里巴巴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
    }
};

擴充套件參考:
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();
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);
            }
        }
    }
}

擴充套件參考:

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-support-library-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 產生記憶體洩漏。