(一) Android 四大元件

AaronZheng發表於2019-03-02

(一) Android 四大元件

Activity

1. 生命週期

(一) Android 四大元件

正常情況下:

  • 啟動 Activity:系統先呼叫onCreate(),然後呼叫onstart(),最後呼叫onResume(),Activity 就此進入執行狀態。
  • 退出 Activity:系統先呼叫onPause(),然後呼叫onStop(),最後呼叫onDestroy(),Activity 就此銷燬。
  • 當前裝置鎖屏或者點選 Home 鍵使程式進入後臺,依次執行onPause()onStop(), 重新回到 Activity,依次執行onRestart()onStart()onResume()(Activity 不被回收的情況下)。
  • 當前 Activity 被 Dialog 主題的 Activity 覆蓋時,執行onPause(),回到前臺執行onResume()
  • 當 Activity 不在前臺或者不可見時被系統回收後,再次回到此 Activity ,系統會依次執行onCreate()onStart()onResume()
  • 啟動一個新 Activity,舊 Activity 會先執行onPause(),然後新 Activity 再啟動。
  • 按是否可見分類:onStart()onStop()
  • 按是否前臺分類:onResume()onPause

注意:當 Activity 彈出對話方塊時,並不會回撥onPause,但是會回撥onStop()

異常情況下:

當系統記憶體不足,或者系統配置發生改變(如旋轉方向),Activity 會被殺死。

  • 由於是異常情況下終止的,系統會呼叫onSaveInstanceState()來儲存當前 Activity 的狀態,這個方法的呼叫時機是在onStop()之前,當 Activity 重新建立後,系統會把銷燬時儲存的 Bundle 物件作為引數傳遞給onCreate()onRestoreInstanceState(),建議在onRestoreInstanceState()中做資料恢復,畢竟專門用來恢復例項狀態的。另外,每個 View 本身都有onSaveInstanceState()onRestoreInstanceState()方法,因此係統都會預設恢復 View 的基本狀態。
  • 防止重新建立 Activity:在 AndroidManifest.xml 中指定android:configChanges="orientation",似乎有些裝置需要多指定一個引數,即android:configChanges:="orientaion|screenSize"

2. 啟動模式

總共 4 種啟動模式:Standard,SingleTop,SingleTask,SingleInstance。

  • Standard:預設的啟動模式,在這種模式下,Activity 會進入啟動它的 Activity 所在的任務棧。
  • SingleTop:如果新 Activity 位於任務棧的棧頂時,Activity 不會被建立,並且它的onNewIntent()方法會被回撥,其餘生命週期方法均不會回撥。
  • SingleTask:如果 Activity 在一個任務棧中存在,那麼多次啟動此 Activity 都不會建立新例項,但系統會回撥onNewIntent()。此外,位於此 Activity 之上的所有 Activity 均會出棧,此時 Activity 位於棧頂。
  • SingleInstance:這種模式下的 Activity 只能單獨存在於一個任務棧中,由於棧內複用特性,此後的請求均不會建立新的例項。

注意:預設情況下,所有 Activity 所需的任務棧的名字為應用的包名,可以在 AndroidManifest.xml 中通過android:taskAffinity=""來指定任務棧。

Service

被啟動的 Service 預設是在主執行緒下工作的,因此如果需要執行耗時操作,應當另開一個子執行緒來執行,以免阻塞主執行緒導致出現 ANR(Application Not Response)。任何 Activity 都可以控制同一個 Service,並且系統中也只會存在一個 Service 例項。

啟動模式

1. startService()

  • 普通模式:在這種模式下啟動 Service 後,即使 Activity 銷燬後,Service 也會在後臺繼續執行任務,直到在 Activity 中手動呼叫stopService()或者在在 Service 類中呼叫stopSelf(),服務才會停止,Activity 無法與 Service 進行通訊。
  • onCreate中可以做一些初始化,onStartCommand()中放置執行任務的程式碼,onDestroy()中進行資源的釋放。
  • 在這種方式下啟動 Service,生命週期是onCreate()->onStartCommand()->onDestroy(),當 Service 已經被啟動後,無論執行多少次startServvice(),都不會走onCreate(),而是會呼叫onStartCommand()

2. bindService()

  • 繫結模式:在這種模式下啟動 Service,當 Activity 銷燬後,Service 也會跟著銷燬,不再使用 Service 時,呼叫unbindService()停止。這種模式下可以進行 Activity 和 Service 的通訊。
  • 這這種方式下啟動 Service,生命週期是onCreate()->onBind()->onUnbind()->onDestroy()onStartCommand()不會有呼叫機會。

注意:當startService()bindService()一起被呼叫後,若想停止服務,必須同時呼叫stopService()unbindService(),這樣服務才會停止,順序沒有嚴格要求,但一定要同時呼叫。

示例

  • 建立一個 Service
public class MyService extends Service {

    private static final String TAG = "MyService";
    private IBinder mBinder;

    @Override
    public IBinder onBind(Intent intent) {
        if (mBinder == null) {
            mBinder = new MyBinder();
        }
        Log.d(TAG, "-----onBind()-----");
        return mBinder;
    }

    @Override
    public boolean onUnbind(Intent intent) {
        Log.d(TAG, "-----onUnbind()-----");
        return super.onUnbind(intent);
    }

    @Override
    public void onCreate() {
        super.onCreate();
        Log.d(TAG, "-----onCreate()-----");
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.d(TAG, "-----onDestroy()-----");
    }

    public class MyBinder extends Binder {

        // 這裡可以定義想要通訊的方法,在 Activity 中可以通過 Binder 例項呼叫
        public void print(String data) {
            Log.d(TAG, "print: " + data);
        }
    }
}
複製程式碼
  • 在 AndroidManifest.xml 中註冊
<!--android:enabled 表示是否啟用,android:exported 表示是否向外界公開-->
        <service
            android:name=".MyService"
            android:enabled="true"
            android:exported="false" />
複製程式碼
  • 在 Activity 中定義 ServiceConnectioin 並進行繫結
public class MainActivity extends AppCompatActivity {

    private ServiceConnection mConnection;
    private MyService.MyBinder mBinder;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mConnection = new MyConnection();
        initView();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        unbindService(mConnection);
    }

    private void initView() {
        Button bindService = findViewById(R.id.btn3);
        Button unbindService = findViewById(R.id.btn4);
        bindService.setOnClickListener(view -> {
            bindService(new Intent(MainActivity.this, MyService.class), mConnection, BIND_AUTO_CREATE);
        });
        unbindService.setOnClickListener(view -> {
            unbindService(mConnection);
        });
    }

    private class MyConnection implements ServiceConnection {

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            // 轉換為自己定義的 Binder
            mBinder = (MyService.MyBinder) service;
            // 呼叫自己定義的方法進行通訊
            mBinder.print("成功通訊");
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            // 這個方法只有在出現異常的時候才會被系統呼叫
        }
    }
}
複製程式碼

前臺服務

服務幾乎都是在後臺執行的,系統優先順序相對比較低,當系統出現記憶體不足時就容易被回收,如果希望服務可以一直保持執行狀態,不會因為記憶體不足而被回收,這時就可以使用前臺服務。與普通服務不同,前臺服務會有一個正在執行的圖示在通知欄裡顯示,類似通知。例如騰訊手機管家等通知,會在通知欄顯示此時手機的記憶體狀態等。

示例:在 Service 中通過startForeground()建立前臺服務,然後在 Activity 中通過startService()bindService()開啟,通過stopService()unbindService()關閉。

public class MyService extends Service {

    private static final String TAG = "MyService";
    private IBinder mBinder;

    @Override
    public void onCreate() {
        super.onCreate();
        createForegroundService();
    }
    
    private void createForegroundService() {
        Intent intent = new Intent(this, MainActivity.class);
        PendingIntent pi = PendingIntent.getActivity(this, 0, intent, 0);
        Notification notification;
        // 使用了建造者模式,將需要定義到的部分提前設定好
        NotificationCompat.Builder builder = new NotificationCompat.Builder(this, "Channel_1")
                .setContentTitle("前臺服務")
                .setContentText("This is content text")
                .setWhen(System.currentTimeMillis())
                .setSmallIcon(R.mipmap.ic_launcher)
                .setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher))
                .setContentIntent(pi);
        // 因為 Android 8.0 新增了 NotificationChannel(通知渠道)
        // 因此需要適配,不然在 8.0 上會顯示不了通知
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
            NotificationChannel channel = new NotificationChannel("Channel_1", "前臺服務", NotificationManager.IMPORTANCE_DEFAULT);
            manager.createNotificationChannel(channel);
            notification = builder.build();
        } else {
            notification = builder.build();
        }
        startForeground(1, notification);
    }
    ......
}    
複製程式碼

IntentService

在前面就知道了,普通服務預設是執行在主執行緒中的,如果在服務裡執行一些耗時操作就容易出現 ANR,當然也可以自己另開執行緒來執行,然後在合適的時機在 Service 內部呼叫stopSelf()來停止。但是這樣稍顯麻煩,為了可以簡單地建立一個非同步的、會自動停止的 Service,Android 提供了 IntentService 類,很好地解決了這個問題。

  • 通過建立一個 Service 繼承自 IntentService,並重寫onHandleIntent()方法即可,在這個方法中便可以處理耗時操作,並且當執行完畢後,Service 會自動停止。

示例

public class MyIntentService extends IntentService {

    public MyIntentService() {
        // 必須呼叫父類有參構造
        super("MyIntentService");
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        Log.d("MyIntentService", "currentThread: " + Thread.currentThread().getName());
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.d("MyIntentService", "-----onDestroy()-----");
    }
}
複製程式碼
  • 當執行完畢後,Service 自動停止,詳情見 Logcat

(一) Android 四大元件

遠端服務

由於遠端服務我並不熟悉,這裡就先落下了,後面學習了再補充上去。

Broadcast

1. 廣播

標準廣播:

  • 是一種完全非同步執行的廣播,所有的廣播接收器之間幾乎沒有任何先後順序。這種廣播效率會比較高,但同時它是無法截斷的。
  • sendBroadcast(intent)—— 表示傳送標準廣播

有序廣播:

  • 是一種同步執行的廣播,廣播發出後,同一時刻只會有一個廣播接收器接收到這條廣播,這種廣播有先後順序,優先順序高的接收器可以先收到廣播,並且前面的接收器可以截斷正在傳遞的廣播。
  • sendOrderedBroadcast(intent, null)—— 表示傳送有序廣播
  • abortBroadcast()—— 表示截斷接收到的廣播

2. 接收器

注意: BroadcastReceiver 生命週期很短, 如果需要在onReceiver()完成一些耗時操作,應該考慮在 Service 中開啟一個新執行緒處理耗時操作,不應該在 BroadcastReceiver 中開啟一個新的執行緒,因為 BroadcastReceiver 生命週期很短,在執行完 onReceiver 以後就結束,如果開啟一個新的執行緒,可能出現 BroadcastRecevier 退出以後執行緒還在,而如果 BroadcastReceiver 所在的程式結束了,該執行緒就會被標記為一個空執行緒,根據 Android 的記憶體管理策略,在系統記憶體緊張的時候,會按照優先順序,結束優先順序低的執行緒,而空執行緒無異是優先順序最低的,這樣就可能導致 BroadcastReceiver 啟動的子執行緒不能執行完成。

動態註冊(監聽網路變化):

  • 定義一個 Receiver 類繼承自 BroadcastReceiver,並重寫父類的 onReceive() 方法,呼叫 addAction() 新增 action 值,當網路狀態發生變化時,系統發出的正式一條值為 android.net.conn.CONNECTIVITY_CHANGE 的廣播,想要監聽什麼廣播便新增相應的 action 值。最後,動態註冊的接受器一定要取消註冊。
public class MainActivity extends AppCompatActivity {

    private IntentFilter intentFilter;
    private NetworkChangeReceiver networkChangeReceiver;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        intentFilter = new IntentFilter();
        intentFilter.addAction("android.net.conn.CONNECTIVITY_CHANGE");
        networkChangeReceiver = new NetworkChangeReceiver();
        registerReceiver(networkChangeReceiver, intentFilter);
    }
    @Override
    protected void onDestroy(){
        super.onDestroy();
        unregisterReceiver(networkChangeReceiver);
    }
    class NetworkChangeReceiver extends BroadcastReceiver{

        @Override
        public void onReceive(Context context, Intent intent){
            ConnectivityManager manager = (ConnectivityManager)
                    getSystemService(Context.CONNECTIVITY_SERVICE);
            NetworkInfo networkInfo = manager.getActiveNetworkInfo();
            if(networkInfo != null && networkInfo.isAvailable()){
                Toast.makeText(context, "網路沒毛病", Toast.LENGTH_SHORT).show();
            }else{
                Toast.makeText(context, "網路不可用", Toast.LENGTH_SHORT).show();
            }
        }
    }
}
複製程式碼
  • onReceive() 方法中,通過 getSystemService() 方法得到了 ConnectivityManager 的例項,這是一個系統服務類,專門用於管理網路連線。然後呼叫它的 getActiveNetworkInfo() 方法可以得到 NetworkInfo 的例項,接著呼叫 NetworkInfo 的 isAvailable() 方法即可判斷當前是否有網路。

  • 最後需要在 AndroidManifest.xml 中註冊訪問系統網路狀態許可權。

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

靜態註冊(監聽系統開機):

  • 使用 Android Studio 提供的快捷方式新建一個廣播接收器,Exported 屬性表示是否允許這個接收器接收本程式以外的廣播,Enabled 屬性表示是否啟用這個接收器。
  • AndroidManifest.xml 中 Android Studio 已經自動註冊好了靜態的廣播接收器,由於需要監聽開機廣播,因此需要宣告接收開機廣播許可權,並且 intent-filter 標籤也需要新增相應的 action 值。
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

<receiver
    android:name=".BootCompleteReceiver"
    android:enabled="true"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED"/>
    </intent-filter>
</receiver>
複製程式碼

靜態註冊與動態註冊的區別

  • 動態註冊廣播不是常駐型廣播,也就是說廣播跟隨 Activity 的生命週期。需要在 Activity 結束前,移除廣播接收器。 靜態註冊是常駐型,也就是說當應用程式關閉後,如果有資訊廣播來,程式也會被系統呼叫自動執行。
  • 當廣播為有序廣播時:
  1. 優先順序高的先接收
  2. 同優先順序的廣播接收器,動態優先於靜態
  3. 同優先順序的同類廣播接收器,靜態:先掃描的優先於後掃描的,動態:先註冊的優先於後註冊的。
  • 當廣播為普通廣播時:
  1. 無視優先順序,動態廣播接收器優先於靜態廣播接收器
  2. 同優先順序的同類廣播接收器,靜態:先掃描的優先於後掃描的,動態:先註冊的優先於後註冊的。

3. 本地廣播

本地廣播用了一個 LocalBroadcastManager 來對廣播進行管理,並提供了傳送廣播和註冊廣播接收器的方法。另外,本地廣播是無法通過靜態註冊的方式來接收的

public class MainActivity extends AppCompatActivity {

    private IntentFilter intentFilter;
    private LocalReceiver localReceiver;
    private LocalBroadcastManager localBroadcastManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        localBroadcastManager = LocalBroadcastManager.getInstance(this);
        Button button = findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = 
                        new Intent("com.example.broadcasttest.LOCAL_BROADCAST");
                localBroadcastManager.sendBroadcast(intent);
            }
        });
        intentFilter = new IntentFilter();
        intentFilter.addAction("com.example.broadcasttest.LOCAL_BROADCAST");
        localReceiver = new LocalReceiver();
        localBroadcastManager.registerReceiver(localReceiver, intentFilter);
    }
    @Override
    protected void onDestroy(){
        super.onDestroy();
        localBroadcastManager.unregisterReceiver(localReceiver);
    }
    class LocalReceiver extends BroadcastReceiver{

        @Override
        public void onReceive(Context context, Intent intent){
            // 編寫邏輯操作
        }
    }
}
複製程式碼

ContentProvider

ContentProvider 是 Android 四大元件之一的內容提供器,它主要的作用就是將程式的內部的資料和外部進行共享,為資料提供外部訪問介面,被訪問的資料主要以資料庫的形式存在,而且還可以選擇共享哪一部分的資料。這樣一來,對於程式當中的隱私資料可以不共享,從而更加安全。ContentProvider 是 Android 中一種跨程式共享資料的重要元件。

1. 使用系統的 ContentProvider

系統的 ContentProvider 有很多,如通話記錄,簡訊,通訊錄等等,都需要和第三方的 App 進行共享資料。既然是使用系統的,那麼 ContentProvider 的具體實現就不需要我們擔心了,使用內容提供者的步驟如下:

  • 獲取 ContentResolver 例項
  • 確定 Uri 的內容,並解析為具體的 Uri 例項
  • 通過 ContentResolver 例項來呼叫相應的方法,傳遞相應的引數,但是第一個引數總是 Uri,它制定了我們要操作的資料的具體地址

可以通過讀取系統通訊錄的聯絡人資訊,顯示在Listview中來實踐這些知識。不要忘記在讀取通訊錄的時候,在清單檔案中要加入相應的讀取許可權。

2. 自定義 ContentProvider

系統的 ContentProvider 在與我們互動的時候,只接受了一個 Uri 的引數,然後根據我們的操作返回給我們結果。系統到底是如何根據一個 Uri 就能夠提供給我們準確的結果呢?只有自己親自實現一個看看了。和之前提到的一樣,想重新自定義自己程式中的四大元件,就必須重新實現一個類,重寫這個類中的抽象方法,在清單檔案中註冊,最後才能夠正常使用。

重新實現 ContentProvider 之後,發現我們重寫了 6 個重要的抽象方法

  • onCreate()
  • query()
  • update()
  • insert()
  • delete()
  • getType()

大部分的方法在資料庫那裡已經見過了,他們內部的邏輯可想而知都是對資料的增刪改查操作,其中這些方法的第一個引數大多都是 Uri 例項。其中有兩個方法比較特殊:

onCreate()方法應該是內容提供者建立的時候所執行的一個回撥方法,負責資料庫的建立和更新操作。這個方法只有我們在程式中獲取 ContentResolver 例項之後準備訪問共享資料的時候,才會被執行。

getType()方法是獲取我們通過引數傳遞進去的 Uri 的 MIME 型別,這個型別是什麼,後面會有例項說明。

內容提供者首先要做的一個事情就是將我們傳遞過來的 Uri 解析出來,確定其他程式到底想訪問哪些資料。Uri 的形式一般有兩種:

  1. 以路徑名為結尾,這種 Uri 請求的是整個表的資料,如: content://com.demo.androiddemo.provider/table1標識我們要訪問 table1 表中所有的資料。
  2. 以 id 列值結尾,這種Uri請求的是該表中和其提供的列值相等的單條資料。content://com.demo.androiddemo.provider/table1/1 標識我們要訪問 table1 表中 _id 列值為 1 的資料。

如果是內容提供器的設計者,那麼我們肯定知道這個程式的資料庫是什麼樣的,每一張表,或者每一張表中的 _id 都應該有一個唯一的內容 Uri 。我們可以將傳遞進來的 Uri 和我們存好的 Uri 進行匹配,匹配到了之後,就說明資料來源已經找到,便可以進行相應的增刪改查操作。

最後

四大元件的學習可以參考第一行程式碼裡面的知識點,都是比較基礎的東西,自己碼出來練習練習即可,沒必要死記,需要用到時腦袋裡有這麼個印象然後知道哪裡可以查就行了,久而久之自然而然就記住了。

相關文章