Android 入門(一)四大組件

知識點摘要:四大組件的使用、Activity 的啓動模式、Service 的 start 和 bindjava

四大組件之 Activity

學習 Android 就不得不講到 Activity 了,畢竟用戶打交道最多的就是 Activity,因此咱們必定要學好它。android

建立 Activity

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

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。github

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");
    }
}
複製代碼

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

onCreate(): 這個回調是必須實現,該回調會在系統建立 Activity 時觸發。能夠在該回調的實現中初始化 Activity 的基本組件,例如:IDE 爲咱們實現了 setContentView(R.layout.activity_main) 方法。服務器

onStart(): 當 onCreate() 執行完成以後,下一個回調始終是 onStart(),這個時候活動進入 Started 狀態。此時活動對用戶是可見的,可是還沒出如今前臺,沒法與用戶進行交互。網絡

onResume(): 系統在活動開始與用戶交互以前調用此回調。此時,活動位於活動堆棧的頂部,並捕獲全部用戶輸入。應用程序的大多數核心功能都是在 onResume() 方法中實現的。app

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() 方法都會被調用。

    注意:在非 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 模式同樣。

    注意:這個 activity 的 onCreate、onStart、onResume 不會被調用,由於它們沒有發生改變。

  • singleTask 模式:

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

    總結來講,singleTask 啓動模式下,要啓動的 Activity 若是位於當前棧內,就會直接複用棧內實例,若是實例位於棧頂,固然能夠直接出棧使用;可是若是不在棧頂,則會將棧頂的元素直接彈出棧,知道找到當前 Activity 的實例。

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

  • singleInstance 模式:

    由 log 日誌能夠知道,啓動 LaunchModeActivity 時,其 TaskId 與 MainActivity 的 TaskId 不一樣。而且在點擊按鈕重啓的時候,調用了 OnNewIntent,而 TaskId 與 HashCode 沒有改變,這就說明了 singleInstance 模式的 Activity 都擁有本身的任務棧。

    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 在整個系統中是全局惟一的。

注意:默認狀況下,全部 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四大啓動模式

相關文章
相關標籤/搜索