Android 入門(一)四大元件

wendraw發表於2019-01-17

知識點摘要:四大元件的使用、Activity 的啟動模式、Service 的 start 和 bind

四大元件之 Activity

學習 Android 就不得不講到 Activity 了,畢竟使用者打交道最多的就是 Activity,所以我們一定要學好它。

建立 Activity

建立方法很簡單右擊你的包名(如 com.example.activitytest) --> New --> Activity --> Empty Activity,會彈出來一個建立活動的視窗。

New Android Activity

在圖中「Activity Name」就是所要建立 Activity 的名字;「Generate Layout File」選項勾選之後,IDE 會自動為我們建立佈局檔案,並且「Layout Name」是佈局檔案的名字;「Launcher Activity」選項勾選後,IDE 會在 ActivityManifest.xml 中將 activity 註冊為啟動介面,也就是我們開啟 app 後顯示的第一個介面;「Package name」是表示 Activity 檔案所存放的位置;「Source Language」則可以選擇 Activity 的程式語言,可以選擇 Java 或者 Kotlin。

Activity 的生命週期

建立一個 Activity 就是這麼簡單,學會了怎麼建立 Activity,我們就可以來學習 Activity 的生命週期。Google 官方給出的生命週期圖如下:

Activity 生命週期圖

想要學習 Activity 的生命週期其實很簡單,只需要建立一個 BaseActivity 類,在並且重寫(override)所有的方法,在方法體中列印出 Log。之後建立新的 Activity 只需要繼承 BaseActivity。

public class BaseActivity extends AppCompatActivity {
    public final String TAG = "Life Cycle - " + getClass().getSimpleName();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(TAG, "**************onCreate**************");
    }

    @Override
    protected void onStart() {
        super.onStart();
        Log.d(TAG, "onStart");
    }

    @Override
    protected void onResume() {
        super.onResume();
        Log.d(TAG, "onResume");
    }

    @Override
    protected void onPause() {
        super.onPause();
        Log.d(TAG, "onPause");
    }

    @Override
    protected void onStop() {
        super.onStop();
        Log.d(TAG, "onStop");
    }

    @Override
    protected void onRestart() {
        super.onRestart();
        Log.d(TAG, "onRestart");
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        Log.d(TAG, "onDestroy");
    }
}
複製程式碼

在整個生命週期中,圖中的那些方法是什麼?都有什麼作用呢?

onCreate(): 這個回撥是必須實現,該回撥會在系統建立 Activity 時觸發。可以在該回撥的實現中初始化 Activity 的基本元件,例如:IDE 為我們實現了 setContentView(R.layout.activity_main) 方法。

onStart(): 當 onCreate() 執行完成之後,下一個回撥始終是 onStart(),這個時候活動進入 Started 狀態。此時活動對使用者是可見的,但是還沒出現在前臺,無法與使用者進行互動。

onResume(): 系統在活動開始與使用者互動之前呼叫此回撥。此時,活動位於活動堆疊的頂部,並捕獲所有使用者輸入。應用程式的大多數核心功能都是在 onResume() 方法中實現的。

onPause(): 當活動失去焦點並進入暫停狀態時,系統呼叫 onPause() 。例如,當使用者點選“後退”或“最近”按鈕時,會出現此狀態。當系統為您的活動呼叫 onPause() 時,它在技術上意味著您的活動仍然部分可見,但大多數情況下表明使用者正在離開活動,並且活動很快將進入“已停止”或“已恢復”狀態。

此時可以做一些儲存資料,停止動畫等工作,注意不能太耗時,因為這會影響到新 Activity 的顯示,onPause 必須先執行完,新的 Activity 的 onResume() 才會執行。

如果使用者期望UI更新,則處於暫停狀態的活動可以繼續更新UI。這種活動的示例包括示出導航地圖螢幕或媒體播放器播放的活動。即使這些活動失去焦點,使用者也希望他們的UI繼續更新。

一旦 onPause() 完成執行,下一個回撥就是 onStop() 或 onResume(),具體取決於活動進入 Paused 狀態後會發生什麼。

onStop(): 當活動不再對使用者可見時,系統呼叫 onStop()。這可能是因為活動被破壞,新活動正在開始,或者現有活動正在進入恢復狀態並且正在覆蓋已停止的活動。在所有這些情況下,停止的活動根本不再可見。

如果活動返回與使用者互動,系統呼叫的下一個回撥是 onRestart(),或者如果此活動完全終止,則由 onDestroy() 呼叫。

onRestart(): 當處於“已停止”狀態的活動即將重新啟動時,系統將呼叫此回撥。在這個回撥之後執行始終是 onStart()。

onDestroy(): 系統在銷燬活動之前呼叫此回撥。此回撥是活動收到的最後一個回撥。通常實現 onDestroy() 以確保在活動或包含它的程式被銷燬時釋放所有活動的資源。

知道了 Activity 的整個生命週期回撥的方法,我們現在對於各種千奇百怪的操作,只需要檢視日誌就能掌握了。比如,按後退、Home、選單鍵,或者再開啟一個新的活動等等,會發生什麼呢?活動會回撥哪些方法?讀者可以自行嘗試,我這裡就不贅述。

還要提醒讀者有一個特殊情況,就是當 activity 中彈出 dialog 對話方塊的時候,activity 不會回撥 onPause。 然而當 activity 啟動 dialog 風格的 activity 的時候,此 activity 會回撥 onPause 方法。

異常情況下的生命週期:

情況一: 比如當系統資源配置發生改變(比如,從豎屏狀態變成橫屏狀態)以及系統記憶體不足時,activity 就會被殺死。

異常情況下的生命週期

當系統配置發生改變之後,Activity 會銷燬,會依此執行 onPause,onStop,onDestory,由於 activity 是在異常情況下終止的,系統會呼叫 onSaveInstance 來儲存當前 activity 的狀態,當 activity 重新建立後,系統會呼叫 onRestoreInstance,並且把 onSaveInstance 方法儲存的 Bundle 物件作為引數同時傳遞給 onRestoreInstance 和 onCreate 方法。

同時,在 onSaveInstanceState 和 nRestoreInstanceState 方法中,系統自動為我們做了一些恢復工作,如:EditText 中使用者輸入的資料,ListView 滾動的位置等,這些 view 相關的狀態,系統都能預設為我們恢復。在 view 的原始碼中,可以看到每個 view 都有 onSaveInstanceState 方法和 onRestoreInstanceState 方法。

情況二: 當系統記憶體不足時,會導致低優先順序的 Activity 被殺死,這時候資料儲存和恢復方法和前面是一致的。一般 Activity 的優先順序是根據是否可見,能否互動來分級的。

優先順序最高的就是,前臺的正在和使用者互動的 Activity。其次是可見但不是前臺也無法與使用者進行互動(比如,在 Activity 中彈出來一個對話方塊),優先順序最低的就是後臺 Activity,也就是已經被執行了 onStop 的 Activity。

防止重新建立 Activity:可以指定 Activity 的 configChange 屬性(android : configChanges = "orientation"),讓系統不會在配置發生變化後,重新建立 Activity。

Activity 的啟動模式

我們已經學會了 Activity 的生命週期,充分了解 Activity 一生的經過,那麼肯定還要知道 Activity 怎麼來的。Activity 給我們提供了有四種啟動模式:standard,singleTop,singleTask,singleInstance。

我們要新建一個 LaunchModeActivity 活動,其中再放入兩個按鈕,一個是 「Restart Self」用來重啟 LaunchModeActivity,還有一個是「Open Other Activity」按鈕用來開啟 OtherLaunchModeActivity,還要建立一個 OtherLaunchModeActivity 活動,它包含一個「Open Father Activity」按鈕用來開啟 LaunchModeActivity。

我們在 BaseActivity 中新增 dumpTaskAffinity 方法,用來列印 taskAffinity 屬性,taskAffinity 又是什麼呢?它 表示此活動對系統中另一個任務的親和力。 此處的字串是任務的名稱,通常是整個包的包名稱。 如果為null,則活動沒有親和力。我們還可以在 AndroidManifest.xml 中為 activity 修改 taskAffinity 屬性。

在 AndroidManifest.xml 中
<activity
        android:name=".activities.LaunchModeActivity"
        android:launchMode="standard"
        android:taskAffinity="com.wendraw.demo.standard" />
複製程式碼
public class BaseActivity extends AppCompatActivity {
    public final String TAG = "Life Cycle - " + getClass().getSimpleName();
    public final String LAUNCHMODE = "Life Cycle(Launch Mode) - " + getClass().getSimpleName();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(TAG, "**************onCreate**************");
        Log.d(LAUNCHMODE, "onCreate: " + getClass().getSimpleName() + "'s TaskId: "
                + getTaskId() + " HashCode: " + this.hashCode());
        dumpTaskAffinity();
    }
    
    ....

    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        Log.d(LAUNCHMODE, "onNewIntent: " + getClass().getSimpleName() + " TaskId: "
                + getTaskId() + " HashCode: " + this.hashCode());
    }

    /**
     * 列印 taskAffinity 屬性
     */
    protected void dumpTaskAffinity() {
        try {
            ActivityInfo info = this.getPackageManager()
                    .getActivityInfo(getComponentName(), PackageManager.GET_META_DATA);
            Log.i(LAUNCHMODE, "taskAffinity:" + info.taskAffinity);
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
    }
}
複製程式碼

為了更直觀的對比,我們在每種模式中操作的順序保持一致,都是從 MainActivity -> 點選按鈕開啟 LaunchModeActivity -> 點選兩次「Restart Self」按鈕重啟 LaunchModeActivity -> 點選「Open Other Activity」按鈕開啟 OtherLaunchModeActivity -> 點選「Open Father Activity」按鈕開啟 LaunchModeActivity。

  • standard 模式:

    由 LaunchModeActivity 的 log 可以看到,每次點選按鈕重新啟動自己時,hashcode 的值都不一樣,也就是說每次啟動一個 Activity 都會重新建立一個例項。再由每個例項的 TaskId 都是一樣的,與其啟動 Activity(MainActivity)的 TaskId 一致,也就是說 standard 模式下, Activity 預設會進入啟動它的 activity 所屬的任務棧中。並且這個 Activity 它的 onCreate(),onStart(),onResume() 方法都會被呼叫。

    Android 入門(一)四大元件

    注意:在非 activity 型別的 context(如 ApplicationContext)並沒有所謂的任務棧,所以不能通過 ApplicationContext 啟動 standard 模式的 activity。

  • singleTop 模式:

    SingleTop 模式下,如果要開啟的 Activity 位於棧頂,那麼這個 Activity 不會被重新建立,會直接從棧頂彈出(由點選兩次重啟按鈕後,LaunchModeActivity 的 HashCode 沒有改變可以得到這個結論),同時 OnNewIntent 方法會被呼叫,通過此方法的引數,我們可以去除當前請求的資訊,且不執行 onCreate、onStart 方法,因為它並沒有發生改變。如果該 Activity 不在棧頂的時候,則情況與 standard 模式相同(通過在 OtherSingleTopActivity 點選按鈕啟動 LaunchModeActivity 可以看出來,HashCode 改變了,且執行了 onCreate、onStart 方法)。

    總結來說,singleTop 模式分3種情況:

    1. 當前棧中已有該 Activity 的例項,並且位於棧頂,這時不會建立新例項,而是複用棧頂物件,並且會將 Intent 物件傳入,回撥 OnNewIntent 方法。
    2. 當前棧中已有該 Activity 的例項,但不位於棧頂,這時會建立新例項,其行為和 Standard 模式一樣。
    3. 當前棧中不存在該 Activity 例項,其行為和 Standard 模式一樣。

    Android 入門(一)四大元件

    注意:這個 activity 的 onCreate、onStart、onResume 不會被呼叫,因為它們沒有發生改變。

  • singleTask 模式:

    由 log 日誌可以知道,點選兩次按鈕重啟 LaunchModeActivity 時,HashCode 沒有改變,也就是說重啟沒有建立新例項,並且會呼叫 OnNewIntent 方法。從 OtherLaunchModeActivity 開啟 LaunchModeActivity 時,此時 LaunchModeActivity 的 OnNewIntent、OnRestart 被呼叫,HashCode 沒有改變,也就是複用了棧內例項,當 LaunchModeActivity 呼叫 OnResume 也就是 Activity 前臺可見後,OtherLaunchModeActivity 執行了 OnStop、OnDestroy 銷燬了。

    總結來說,singleTask 啟動模式下,要啟動的 Activity 如果位於當前棧內,就會直接複用棧內例項,如果例項位於棧頂,當然可以直接出棧使用;但是如果不在棧頂,則會將棧頂的元素直接彈出棧,知道找到當前 Activity 的例項。

    Android 入門(一)四大元件

    補充: 由 taskAffinity 的值沒變,並且是預設的包名,也就是說目前採用的是棧內複用模式。 如果在 AndroidManifest.xml 中,將 LaunchModeActivity 的 taskAffinity 屬性設定成 "com.wendraw.demo.singletask",此時 log 會有所改變,在啟動 LaunchModeActivity 的時候TaskId 與 MainActivity 的不同了,而OtherLaunchModeActivity 沒有改變 taskAffinity ,其 TaskId 與 MainActivity 相同。這就說明了,在 singleTask 啟動模式下,會根據 taskAffinity 的值來為 Activity 分配任務棧。

    Android 入門(一)四大元件

  • singleInstance 模式:

    由 log 日誌可以知道,啟動 LaunchModeActivity 時,其 TaskId 與 MainActivity 的 TaskId 不同。並且在點選按鈕重啟的時候,呼叫了 OnNewIntent,而 TaskId 與 HashCode 沒有改變,這就說明了 singleInstance 模式的 Activity 都擁有自己的任務棧。

    Android 入門(一)四大元件

    ingleInstance 模式又叫全域性唯一模式,如果我們將 LaunchModeActivity 設定為 singleInstance,並且設定其 intent-filter 屬性的 action android:name="com.wendraw.demo.singleinstance"、
    category android:name="android.intent.category.DEFAULT",然後在 MainActivity 中啟動 LaunchModeActivity(不管是顯式 Intent 還是隱式 Intent) -> 點選 Home 鍵回到桌面。開啟另一個 SingleInstanceDemo 應用,它很簡單隻有一個 MainActivity,還有一個按鈕用來隱式 Intent 啟動 LaunchModeActivity,點選按鈕後直接啟動了 learnfourmaincomponents 的 LaunchModeActivity。我們可以看到呼叫了 OnNewIntent、OnRestart,而且 TaskId、HashCode 與之前的一致,也就證明了singleInstance 在整個系統中是全域性唯一的。

    Android 入門(一)四大元件

注意:預設情況下,所有 activity 所需的任務棧的名字為應用的包名,可以通過 activity 指定 TaskAffinity 屬性來指定任務棧,當然這個屬性不能和包名相同,否則就沒有任何意義了不是嗎?

Activity 與 Fragment

Fragment(碎片)是一種可以嵌入在活動當中的 UI 片段,它能讓程式更加合理和充分的利用大螢幕空間。關於 Fragment 的基本用法,我就不贅述了,不知道的讀者可以自行 Google。既然 Fragment 是嵌入在活動中的,那我們主要來學習 Activity 和 Fragment 的生命週期。

我們在 Android 官網 可以找到「Fragment 的生命週期圖」和「Activity 與 Fragment 的生命週期的對比圖」如下:

Fragment Life Cycle

Activity and Fragment Life Cycle

先建立一個 BaseFragment 繼承 Fragment 並實現生命週期上的所有方法

public class BaseFragment extends Fragment {

    private final String TAG = "Life Cycle -" + this.getClass().getSimpleName();

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        Log.d(TAG, "onAttach");
    }

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

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
        @Nullable Bundle savedInstanceState) {
        Log.d(TAG, "onCreateView");
        return super.onCreateView(inflater, container, savedInstanceState);
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        Log.d(TAG, "onActivityCreated");
    }

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

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

    /************************* Fragment is active ***************************/

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

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

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

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

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

複製程式碼

然後建立 FirstFragment 並繼承 BaseFragment,這是四大元件中唯一一個不需要在 Manifest.xml 中進行註冊的。

public class FirstFragment extends BaseFragment {

    public FirstFragment() {
        // Required empty public constructor
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_first, container, false);
    }

}
複製程式碼

Fragment 的使用方法也很簡單,有兩種方式,第一種是靜態載入,只需要在 Activity 的 layout.xml 中新增 fragment 控制元件,並指定其 name 為 FirstFragment 即可。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".activities.SecondActivity">

    <TextView
        android:id="@+id/textView"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:text="I'm Activity Content." />

    <fragment
        android:id="@+id/first_fragment"
        android:name="com.wendraw.learnfourmaincomponents.fragments.FirstFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</LinearLayout>
複製程式碼

第二種方式是動態載入,先在 layout 中新增一個 FrameLayout 控制元件,然後再在 SecondActivity 中,使用程式碼進行動態替換。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".activities.SecondActivity">

    <TextView
        android:id="@+id/textView"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:text="I'm Activity Content." />

    <FrameLayout
        android:id="@+id/fragment_layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</LinearLayout>
複製程式碼
public class SecondActivity extends BaseActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);

        //動態替換 Fragment
        FragmentManager fragmentManager = getSupportFragmentManager();
        FragmentTransaction transaction = fragmentManager.beginTransaction();
        transaction.replace(R.id.fragment_layout, new FirstFragment());
        transaction.commit();
    }
}
複製程式碼

雖然實現的方式不一樣,但執行程式再開啟 SecondActivity 之後,其效果都是如下圖所示,灰色的部分是 Fragment,白色的部分還是屬於 Activity。

FragmentActivity
Activity And Fragment

通過 MainActivity 點選 Button 跳轉到 SecondActivity,並且 SecondActivity 包含一個 FirstFragment,可以看出 Activity 和 Fragment 的生命週期的關係與上圖描述的一致。

Fragment Life Cycle

Activity 與 menu 建立先後順序

官方介紹,如果開發的應用適用於 Android 2.3.x(API 級別 10)或者更低版本,則當使用者首次點選選單選項(也就是那三個點的圖示)時,系統會呼叫 onCreateOptionMenu() 來建立選單;如果開發的應用適用於 Android 3.0 及更高版本,則將在系統啟動 Activity 時呼叫 onCreateOptionMenu() 建立選單,因為 Android 3.0 之後,可以在 ActionBar 顯示選單的 item。

筆者只測試了第二種情況,可以看出來是當 Activity 呼叫 onResume 之後才開始建立選單的。如果讀者感興趣可以自行測試 Android 2.3.x 及其一下的版本的情況。

Activity 與 menu 建立的先後順序

四大元件之 Service

Service 是一個可以在後臺執行長時間執行操作而不提供使用者介面的應用元件。服務可由其他應用元件啟動,而且即使使用者切換到其他應用,服務仍將在後臺繼續執行。 此外,元件可以繫結到服務,以與之進行互動,甚至是執行程式間通訊 (IPC)。 例如,服務可以處理網路事務、播放音樂,執行檔案 I/O 或與內容提供程式互動,而所有這一切均可在後臺進行

建立 Service 其實很簡單,右鍵包名 com.wendraw.learnfourmaincomponents -> New -> Service,發現有 Service 和 Service(IntentService) 可以選擇,那我們應該選擇哪一個呢?Service 一般分為兩種形式:

啟動: 當應用元件(如 Activity)通過呼叫 startService() 啟動服務時,服務即處於“啟動”狀態。一旦啟動,服務即可在後臺無限期執行,即使啟動服務的元件已被銷燬也不受影響。 已啟動的服務通常是執行單一操作,而且不會將結果返回給呼叫方。例如,它可能通過網路下載或上傳檔案。 操作完成後,服務會自行停止執行。

繫結: 當應用元件通過呼叫 bindService() 繫結到服務時,服務即處於“繫結”狀態。繫結服務提供了一個客戶端-伺服器介面,允許元件與服務進行互動、傳送請求、獲取結果,甚至是利用程式間通訊 (IPC) 跨程式執行這些操作。 僅當與另一個應用元件繫結時,繫結服務才會執行。 多個元件可以同時繫結到該服務,但全部取消繫結後,該服務即會被銷燬。

雖然我們分開介紹這兩種形式的服務,但是我們建立的服務可以同時包含這兩種形式,也就是說,它既可以是啟動服務(以無限期執行),也允許繫結。問題只是在於您是否實現了一組回撥方法:onStartCommand()(允許元件啟動服務)和 onBind()(允許繫結服務)。

注意:服務在其託管程式的主執行緒中執行,它既不建立自己的執行緒,也不在單獨的程式中執行(除非另行指定)。 這意味著,如果服務將執行任何 CPU 密集型工作或阻止性操作(例如 MP3 播放或聯網),則應在服務內建立新執行緒來完成這項工作。通過使用單獨的執行緒,可以降低發生“應用無響應”(ANR) 錯誤的風險,而應用的主執行緒仍可繼續專注於執行使用者與 Activity 之間的互動。

跟前面已經學習過的四大元件一樣,我們主要來關注 Service 的生命週期。同樣在 Android 官網 的 Service 找到如下生命週期圖,左邊表示使用啟動方式的 Service 的生命週期,右邊表示使用繫結方式的 Service 的生命週期。

Service 生命週期

與 Activity 一樣新建一個 Service,並重寫生命週期中的方法,然後在 AndroidManifest.xml 中註冊。

class MyService : Service() {
    private val TAG = "Life Cycle - " + javaClass.simpleName

    override fun onCreate() {
        Log.d(TAG, "**************onCreate**************")
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        Log.d(TAG, "onStartCommand")
        return super.onStartCommand(intent, flags, startId)
    }

    override fun onBind(intent: Intent?): IBinder? {
        Log.d(TAG, "onBind")
        return MyBinder()
    }

    override fun onUnbind(intent: Intent?): Boolean {
        Log.d(TAG, "onUnbind")
        return super.onUnbind(intent)
    }

    override fun onDestroy() {
        Log.d(TAG, "onDestroy")
    }

    interface MyIBinder {
        fun invokeMethodInMyService()
    }

    inner class MyBinder : Binder(), MyIBinder {

        fun stopService(serviceConnection: ServiceConnection) {
            unbindService(serviceConnection)
        }

        override fun invokeMethodInMyService() {
            for (i in 0..19) {
                println("service is opening")
            }
        }

    }
}
複製程式碼
AndroidManifest.xml
<service
       android:name=".services.MyService"
       android:enabled="true"
       android:exported="true" />
複製程式碼

然後在 Activity 中選擇需要啟動 Service 的方式

class ServiceActivity : AppCompatActivity() {

    private lateinit var mBinder: MyService.MyBinder
    private lateinit var mServiceConnection: ServiceConnection

    private var isBind = false  //標記 Service 是否已經繫結

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_service)

        //使用 startService 方式啟動 Service
        start_service_btn.setOnClickListener {
            val intent = Intent(this@ServiceActivity, MyService::class.java)
            startService(intent)
        }

        //停止 Service
        stop_service_btn.setOnClickListener {
            val intent = Intent(this@ServiceActivity, MyService::class.java)
            stopService(intent)
        }

        //使用 bindService  方式啟動 Service
        bind_service_btn.setOnClickListener {
            isBind = true
            mServiceConnection = MyServiceConnection()
            val intent = Intent(this@ServiceActivity, MyService::class.java)
            bindService(intent, mServiceConnection, Context.BIND_AUTO_CREATE)
        }

        //解綁 Service
        unbind_service_btn.setOnClickListener {
            unbindService(mServiceConnection)
        }

        //使用 startService 方式啟動 IntentService
        start_intent_service_btn.setOnClickListener {
            //列印主執行緒的 id
            Log.d("ServiceActivity", "Thread id is " + Thread.currentThread().id)
            val intent = Intent(this@ServiceActivity, MyIntentService::class.java)
            startService(intent)
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        if (isBind) {
            //當活動被銷燬時,需要解綁 Service
            unbindService(mServiceConnection)
        }
    }

    inner class MyServiceConnection : ServiceConnection {

        override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
            Log.d("MyService", "onServiceConnected")
            mBinder = service as MyService.MyBinder
        }

        override fun onServiceDisconnected(name: ComponentName?) {
            Log.d("MyService", "onServiceDisconnected")
        }
    }
}
複製程式碼

在 ServiceActivity 中有五個按鈕,分別是啟動服務、停止服務、繫結服務、解綁服務和啟動 IntentService。

ServiceActivity 介面
ServiceActivity 介面

我們先點選啟動服務,再點選停止服務按鈕,也就是用 startService() 啟動服務,使用我們可以看到如下日誌:

啟動服務

如果我們依此點選繫結服務、解綁服務,使用繫結形式啟動服務,可以得到如下日誌:

繫結服務

可以看到與官方給出的生命週期圖是一致的。

我們還可以嘗試使用 startService、bindService 方式進行混合啟動服務,先點選啟動服務按鈕 -> 點選繫結服務按鈕,此時 MyService 是一個無限期執行的、繫結的服務,如果此時像退出服務怎麼半呢?需要點選「STOP SERVICE」、「UNBIND SERVICE」兩個按鈕,才會執行 onDestroy 方法,表示服務已經被銷燬。

混用啟動和繫結

四大元件之 Content Provider

下面是摘自「第一行程式碼」中的介紹:

內容提供器(Content Provider) 主要用於在不同的應用程式之間實現資料共享的功能,它提供了一套完整的機制,允許一個程式訪問另一個程式中的資料,同時還能保證資料被訪問的安全性。目前,使用內容提供器是 Android 實現跨程式共享資料的標準方式。

內容提供器作為四大元件之一,我們肯定要學習一波,但是在日常開發中我們用到就是讀取電話簿之類的,所以也沒有必要學習的太過深入,學會增刪查改即可,那麼接下來就一起學習一下對本地電話簿的增刪查改吧。

我們先在 layout.xml 中新增 ListView 用來展示獲取到的聯絡人,query_btn 用來查詢電話簿,insert_btn 用來插入資料。

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".activities.ContentProviderActivity">

    <Button
        android:id="@+id/query_btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        android:text="Query"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <Button
        android:id="@+id/insert_btn"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="8dp"
        android:text="Insert"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/query_btn"
        app:layout_constraintTop_toTopOf="parent" />

    <ListView
        android:id="@+id/content_provider_list_view"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_marginStart="8dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginBottom="8dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/insert_btn" />

</android.support.constraint.ConstraintLayout>
複製程式碼

然後在 Manifest.xml 中申請讀寫聯絡人資訊的許可權

<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
複製程式碼

但是讀寫聯絡人資訊是非常敏感的,所以 Android 要求我們動態的申請許可權,不僅僅在 Manifest 中申明。

class ContentProviderActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_content_provider)

        //檢查許可權
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS)
                != PackageManager.PERMISSION_GRANTED) {
            ActivityCompat.requestPermissions(this,
                    arrayOf(Manifest.permission.READ_CONTACTS), 1)
        } else {
            //讀取聯絡人資訊
        }
    }

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>,  grantResults: IntArray) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        when (requestCode) {
            1 -> {
                if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    readContacts()
                } else {
                    Toast.makeText(this, "You denied the permission", Toast.LENGTH_SHORT).show()
                }
            }
        }
    }
}
複製程式碼

我們已經獲取了讀寫聯絡人的許可權,接下來就可以愉快的獲取和修改聯絡人資訊了。

class ContentProviderActivity : AppCompatActivity() {

    private lateinit var mAdapter: ArrayAdapter<String>
    private val mContactsList: ArrayList<String> = ArrayList()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_content_provider)

        mAdapter = ArrayAdapter(this, R.layout.simple_list_item, mContactsList)
        content_provider_list_view.adapter = mAdapter
        
        ...
        
        query_btn.setOnClickListener {
            readContacts()
        }

        insert_btn.setOnClickListener {
            insertContact("wendraw", "86-13355550000")
        }
    }

    //獲取聯絡人資訊
    private fun readContacts() {
        var cursor: Cursor? = null
        try {
            //查詢聯絡人
            cursor = contentResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
                    null, null, null, null)
            if (cursor != null) {
                while (cursor.moveToNext()) {
                    //獲取聯絡人姓名
                    val displayName = cursor.getString(cursor.getColumnIndex(
                            ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME
                    ))
                    //獲取聯絡人電話
                    val displayPhoneNumber = cursor.getString(cursor.getColumnIndex(
                            ContactsContract.CommonDataKinds.Phone.NUMBER
                    ))
                    mContactsList.add(displayName + "\n" + displayPhoneNumber)
                }
                mAdapter.notifyDataSetChanged()
            }
        } catch (e: Exception) {
            e.printStackTrace()
        } finally {
            cursor?.close()
        }
    }
    
    //新增聯絡人資訊
    private fun insertContact(name: String, phoneNumber: String) {
        // 建立一個空的ContentValues
        val values = ContentValues()
        // 向RawContacts.CONTENT_URI執行一個空值插入,
        // 目的是獲取系統返回的rawContactId
        val rawContactUri = contentResolver.insert(ContactsContract.RawContacts.CONTENT_URI, values)
        val rawContactId = ContentUris.parseId(rawContactUri)

        //清空資料
        values.clear()
        values.put(ContactsContract.Data.RAW_CONTACT_ID, rawContactId)
        //設定內容型別
        values.put(ContactsContract.Data.MIMETYPE, StructuredName.CONTENT_ITEM_TYPE)
        //設定聯絡人姓名
        values.put(StructuredName.GIVEN_NAME, name)
        // 向聯絡人URI新增聯絡人名字
        contentResolver.insert(ContactsContract.Data.CONTENT_URI, values)

        //清空資料
        values.clear()
        values.put(ContactsContract.Data.RAW_CONTACT_ID, rawContactId)
        //設定電話型別
        values.put(ContactsContract.Data.MIMETYPE, Phone.CONTENT_ITEM_TYPE)
        //設定聯絡人電話
        values.put(Phone.NUMBER, phoneNumber)
        // 向聯絡人URI新增聯絡人電話
        contentResolver.insert(ContactsContract.Data.CONTENT_URI, values)

        Toast.makeText(this, "聯絡人資料新增成功", Toast.LENGTH_SHORT)
                .show()
    }

    ...
}
複製程式碼

點選 query_btn 按鈕查詢手機內的所有聯絡人並展示到 ListView

query contact
查詢聯絡人

再點選 insert_btn 按鈕,將名字 wendraw ,電話 86-13355550000 插入,點選 query_btn 按鈕查詢手機內的所有聯絡人,我們可以看到聯絡人順利插入到電話簿當中。你還可以去手機中的通訊錄檢視,也能找到我們剛剛插入的聯絡人。

insert contact
插入聯絡人

結束

至此,我們注重學習了四大元件的生命週期,當然通過這一篇文章就能完全掌握四大元件是不現實的,但是我相信通過對元件生命週期的學習,會有助於接下來的學習。

本文的所有程式碼都已上傳 gayhub 歡迎下載 LearnFourMainComponents

參考

Android 四大元件

Android 官網

徹底弄懂Activity四大啟動模式

相關文章