探究Activity

號外、號外,以後本博客的相關代碼,都會在github中的AndroidKnowledgeSummary展現。當然了,有的時候有,有的就不必寫或是沒法兒寫。github這麼牛X的地方,得參與起來!
手頭有幾本書,《Android羣英傳》、《Android開發藝術探索》、《第一行代碼》、+Android官方開發指南。接下來可能會盡量按照公認的知識點順序,系統化的總結下Android的內容。但也不排除插播其他流行框架、新技術、甚至是胡謅的東西。反正就是怎麼爽怎麼來,是不是有點兒小任性…O(∩_∩)O哈哈~
本篇主要內容:1、怎麼啓動一個Activity。2、Activity生命週期(異常處理)。3、intent過濾器。4、Activity的Flags。5、Activity的Affinity。6、adb命令行查看activity任務棧信息。

1、爲什麼先是Activity

  • 關於Android系統是什麼,我也不夠水平講,看下圖,來自於Android官方平臺架構介紹
    Android系統
  • Activity是Android四大組件之一,也是用戶直接感受到的,一般叫“活動”,我們甚至可以直接叫“界面”。
  • 正常情況下,從桌面啓動一個app,都是進入一個“界面”,所以我們就先從先看到的東西開始說起。
  • 應用組件是 Android 應用的基本構建基塊。分爲四大,Activity、Service、ContentProvider、 BroadcastReceiver。

2、啓動Activity

  • 如果我們使用Android Studio,通過“New Project”創建一個新的Android project,在app目錄下src/main的AndroidManifest.xml文件中(此處說的是Project窗口模式下)。MainActivity就是我們打開app後啓動的第一個Activity,intent-filter表示的是使用“隱式Intent”啓動Activity。下面會詳細介紹,此處我們先去看“顯式”啓動的方式。
<activity android:name=".MainActivity">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>
  • 顯示啓動Activity,也是我們最常用的方式。經典方法如下。
Intent intent = new Intent(this, SecondActivity.class);
startActivity(intent);

如果使用Android Studio中New Activity創建SecondActivity的話,這時就可以直接打開Second Activity了。
Intent是Android程序中各組件之間進行交互的一種重要方式,它不僅可以指明當前組件想要執行的操作,還可以在不同的傳遞數據。一般用於啓動Activity、Service、Broadcast。分爲顯式隱式 兩種情況,上面的用法就是顯式寫法的一種情況。
下面是隱式寫法的一種
根據上面已經有的內容,在AndroidManifest.xml中修改SecondActivity的標記。

<activity android:name=".MainActivity">
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />

        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>
<activity android:name=".SecondActivity">
    <intent-filter>
        <action android:name="com.breezehan.knowledge.second" />
        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
</activity>
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button button = (Button) findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent();
                intent.setAction("com.breezehan.knowledge.second");
                startActivity(intent);
            }
        });
    }
}

點擊MainActivity中的button就可以啓動SecondActivity了。
其中只是setAction就成功了,Category好像沒用,那我們把在上方代碼中12 行添加一行,成品如下:

Intent intent = new Intent();
intent.setAction("com.breezehan.knowledge.second");
intent.addCategory("com.breezehan.knowledge.mycategory");
startActivity(intent);

運行,點擊MainActivity中的button。
不好,崩潰了!

Process: com.breezehan.knowledge, PID: 7521
android.content.ActivityNotFoundException: No Activity found to handle Intent { act=com.breezehan.knowledge.second cat=[com.breezehan.knowledge.mycategory] }

沒有Activity可以響應這個Intent,因爲我們在剛剛的Intent中添加了一個Category,但是SecondActivity的中並沒有聲明,不存在去哪兒響應。
所以在SecondActivity的清單文件中改動。這樣就可以啓動了。前面開始的時候沒有addCategory也能成功,是因爲startActvity的時候會自動添加一個默認的android.intent.category.DEFAULT。

<activity android:name=".SecondActivity">
    <intent-filter>
        <action android:name="com.breezehan.knowledge.second" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="com.breezehan.knowledge.mycategory" />
    </intent-filter>
</activity>

3、更多隱式Intent相關

隱式 Intent 指定能夠在可以執行相應操作的設備上調用任何應用的操作。 如果您的應用無法執行該操作而其他應用可以,且您希望用戶選取要使用的應用,則使用隱式 Intent 非常有用。

接收隱式Intent是根據 < intent-filter > 過濾器決定的,每個應用組件可以聲明一個或多個 Intent 過濾器。每個 Intent 過濾器均根據 Intent 的操作action、數據data和類別category指定自身接受的 Intent 類型。 僅當隱式 Intent 可以通過 Intent 過濾器之一傳遞時,系統纔會將該 Intent 傳遞給應用組件。
分析 < intent-filter > 內部元素:

  1. < action >
    要指定接受的 Intent 操作,也可以聲明多個此類元素。例如:
<intent-filter>
    <action android:name="android.intent.action.EDIT" />
    <action android:name="android.intent.action.VIEW" />
    ...
</intent-filter>

要通過此過濾器,您在 Intent 中指定的操作必須與過濾器中列出的某一操作匹配。

An < intent-filter > element must contain one or more < action > elements.

2.< category>
一個包含應處理 Intent 組件類型的附加信息的字符串。 至少必須有一個Default(android.intent.category.DEFAULT),也可以聲明多個此類元素。

In order to receive implicit intents, you must include the CATEGORY_DEFAULT category in the intent filter.

例如:

<intent-filter>
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    ...
</intent-filter>

若要使 Intent 通過類別測試,則 Intent 中的每個類別均必須與過濾器中的類別匹配。反之則未必然,Intent 過濾器聲明的類別可以超出 Intent 中指定的數量,且 Intent 仍會通過測試。 因此,不含類別的 Intent 應當始終會通過此測試,無論過濾器中聲明何種類別均是如此。

注:Android 會自動將 CATEGORY_DEFAULT 類別應用於傳遞給 startActivity() 和 startActivityForResult() 的所有隱式 Intent。因此,如需 Activity 接收隱式 Intent,則必須將 “android.intent.category.DEFAULT” 的類別包括在其 Intent 過濾器中(如上文的 < intent-filter > 示例所示)。

3.< data >
使用一個或多個指定數據 URI 各個方面(scheme、host、port、path 等)和 MIME 類型的屬性,聲明接受的數據類型。
可以不聲明,也可以是多個。

<data android:scheme="string"
      android:host="string"
      android:port="string"
      android:path="string"
      android:pathPattern="string"
      android:pathPrefix="string"
      android:mimeType="string" />

< scheme>://< host >:< port>/< path>

例如:

content://com.example.project:200/folder/subfolder/etc

在此 URI 中,架構是 content,主機是 com.example.project,端口是 200,路徑是 folder/subfolder/etc。
下面是官方Intent 和 Intent 過濾器介紹
在 < data> 元素中,上述每個屬性均爲可選,但存在線性依賴關係:

  • 如果未指定架構,則會忽略主機。
  • 如果未指定主機,則會忽略端口。
  • 如果未指定架構和主機,則會忽略路徑。

將 Intent 中的 URI 與過濾器中的 URI 規範進行比較時,它僅與過濾器中包含的部分 URI 進行比較。 例如:

  • 如果過濾器僅指定架構,則具有該架構的所有 URI 均與該過濾器匹配。
  • 如果過濾器指定架構和權限,但未指定路徑,則具有相同架構和權限的所有 URI 都會通過過濾器,無論其路徑如何均是如此。
  • 如果過濾器指定架構、權限和路徑,則僅具有相同架構、權限和路徑的 URI 纔會通過過濾器。

注:路徑規範可以包含星號通配符 (*),因此僅需部分匹配路徑名即可。

數據測試會將 Intent 中的 URI 和 MIME 類型與過濾器中指定的 URI 和 MIME 類型進行比較。 規則如下:

  • 僅當過濾器未指定任何 URI 或 MIME 類型時,不含 URI 和 MIME 類型的 Intent 纔會通過測試。
  • 對於包含 URI 但不含 MIME 類型(既未顯式聲明,也無法通過 URI 推斷得出)的 Intent,僅當其 URI 與過濾器的 URI 格式匹配、且過濾器同樣未指定 MIME 類型時,纔會通過測試。
  • 僅當過濾器列出相同的 MIME 類型且未指定 URI 格式時,包含 MIME 類型、但不含 URI 的 Intent 纔會通過測試。
  • 僅當 MIME 類型與過濾器中列出的類型匹配時,同時包含 URI 類型和 MIME 類型(通過顯式聲明,或可以通過 URI 推斷得出)的 Intent 纔會通過測試的 MIME 類型部分。 如果 Intent 的 URI 與過濾器中的 URI 匹配,或者如果 Intent 具有 content: 或 file: URI 且過濾器未指定 URI,則 Intent 會通過測試的 URI 部分。 換言之,如果過濾器只是列出 MIME 類型,則假定組件支持 content: 和 file: 數據。

比如一些社交共享應用的片段。

<activity android:name="MainActivity">
    <!-- This activity is the main entry, should appear in app launcher -->
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>

<activity android:name="ShareActivity">
    <!-- This activity handles "SEND" actions with text data -->
    <intent-filter>
        <action android:name="android.intent.action.SEND"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <data android:mimeType="text/plain"/>
    </intent-filter>
    <!-- This activity also handles "SEND" and "SEND_MULTIPLE" with media data -->
    <intent-filter>
        <action android:name="android.intent.action.SEND"/>
        <action android:name="android.intent.action.SEND_MULTIPLE"/>
        <category android:name="android.intent.category.DEFAULT"/>
        <data android:mimeType="application/vnd.google.panorama360+jpg"/>
        <data android:mimeType="image/*"/>
        <data android:mimeType="video/*"/>
    </intent-filter>
</activity>

我們可以在自己的MainActivity中測試情況。下面是簡單的幾種情況。
MainActivity的button

        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent();
                intent.setAction("com.breezehan.knowledge.second");
                intent.setDataAndType(Uri.parse("http://www.baidu.com"), "image/*");
//                intent.setData(Uri.parse("http://www.baidu.com"));
//                intent.setType("image/*");
//                intent.addCategory("com.breezehan.knowledge.mycategory");
//                intent.setAction(Intent.ACTION_SEND);
//                intent.setType("text/plain");
//                intent.setDataAndType(Uri.parse("http:"),"text/plain");
//                intent.setDataAndType(Uri.parse("content:"),"text/plain");
//                intent.setAction(intent.ACTION_SEND_MULTIPLE);
//                intent.setType("image/*");
                if (intent.resolveActivity(getPackageManager()) != null) {
                    startActivity(intent);
                }
            }
        });

AndroidManifest.xml中

<activity android:name=".SecondActivity">
    <intent-filter>
        <action android:name="com.breezehan.knowledge.second" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="com.breezehan.knowledge.mycategory" />

        <data android:scheme="http" android:mimeType="image/*"/>
    </intent-filter>
</activity>

4、Activity生命週期

先上一個官圖,來點兒說服力。
官方Activity4lifecycle
onCreate:創建Activity時調用。初始化工作,如setContentView加載佈局,初始化數據等。
onRestart:Activity從不可見(停止)狀態回到可見時調用。重新啓動。
onStart:在 Activity 即將對用戶可見之前調用。此時用戶還看不到界面。
onResume:開始與用戶進行交互之前調用。 此時,Activity 處於 Activity 堆棧的頂層,並具有用戶輸入焦點。跟onStart的區別:onStart時Activity還在後臺沒用出現;onResume時Activity已經可見。
onPause:即將啓動或恢復另一個Activity時調用。通常會執行一些釋放CPU資源和保存數據的操作。但一定要迅速,因爲它執行後纔會進入新的Activity。
onStop:在 Activity 對用戶不再可見時調用。如果 Activity 被銷燬,或另一個 Activity(一個現有 Activity 或新 Activity)繼續執行並將其覆蓋,就可能發生這種情況。注意 如果在onResume之後出現的是一個對話框,那麼只會執行onPause,不會執行onStop,因爲部分可見。
onDestroy:在 Activity 被銷燬前調用。這是 Activity 將收到的最後調用。
以上7種狀態,基本上可以分爲3種情況:
1. Activity的整個生命週期 ,onCreate和onDestroy之間。在onCreate種初始化,在onDestory種釋放資源。
2. Activity的可見生命週期,onStart和onStop之間。
3. Activity的前臺生命週期,onResume和onPause之間。可以與用戶交互。

下面寫個例子,直觀感受下Activity生命週期。
改造剛纔的MainActivity,在layout的activity_main的加一個button。
在MainActivity和SecondActivity總override上面的7種方法。並打印log日誌。
如MainActivity中:

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.i(TAG, "onCreate: ");
        setContentView(R.layout.activity_main);
        ...
        findViewById(R.id.button2).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startActivity(new Intent(MainActivity.this,SecondActivity.class));
            }
        });
    }

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

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

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

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

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

    @Override
    protected void onDestroy() {
        super.onDestroy();
        Log.i(TAG, "onDestroy: ");
    }
}

SecondActivity也是如此。這裏Android Studio有個快捷鍵。在設置TAG常量的地方,打出logt後直接TAB鍵就會自動生成。而像onDestroy中打出logi,再TAB鍵也會自動生成。注意了,此處又裝X哈哈!
此時點擊按鈕button查看生命週期。
首次運行進入時,

5456-5456/com.breezehan.knowledge I/MainActivity: onCreate:
5456-5456/com.breezehan.knowledge I/MainActivity: onStart:
5456-5456/com.breezehan.knowledge I/MainActivity: onResume:

點擊button進入SecondActivity時,含上面的打印部分。

5456-5456/com.breezehan.knowledge I/MainActivity: onCreate:
5456-5456/com.breezehan.knowledge I/MainActivity: onStart:
5456-5456/com.breezehan.knowledge I/MainActivity: onResume:
5456-5456/com.breezehan.knowledge I/MainActivity: onPause:
5456-5456/com.breezehan.knowledge I/SecondActivity: onCreate:
5456-5456/com.breezehan.knowledge I/SecondActivity: onStart:
5456-5456/com.breezehan.knowledge I/SecondActivity: onResume:
5456-5456/com.breezehan.knowledge I/MainActivity: onStop:

這裏可以看出一個Activity的生命週期,和打開另一個Activity時的情況。如當前從Main打開進入Second時,onPause(Main)->onCreate(Second)->onStart(Second)->onResume(Second)->onStop(Main)。
連續放回兩次的完整打印。

5456-5456/com.breezehan.knowledge I/MainActivity: onCreate:
5456-5456/com.breezehan.knowledge I/MainActivity: onStart:
5456-5456/com.breezehan.knowledge I/MainActivity: onResume:
5456-5456/com.breezehan.knowledge I/MainActivity: onPause:
5456-5456/com.breezehan.knowledge I/SecondActivity: onCreate:
5456-5456/com.breezehan.knowledge I/SecondActivity: onStart:
5456-5456/com.breezehan.knowledge I/SecondActivity: onResume:
5456-5456/com.breezehan.knowledge I/MainActivity: onStop:
5456-5456/com.breezehan.knowledge I/SecondActivity: onPause:
5456-5456/com.breezehan.knowledge I/MainActivity: onRestart:
5456-5456/com.breezehan.knowledge I/MainActivity: onStart:
5456-5456/com.breezehan.knowledge I/MainActivity: onResume:
5456-5456/com.breezehan.knowledge I/SecondActivity: onStop:
5456-5456/com.breezehan.knowledge I/SecondActivity: onDestroy:
5456-5456/com.breezehan.knowledge I/MainActivity: onPause:
5456-5456/com.breezehan.knowledge I/MainActivity: onStop:
5456-5456/com.breezehan.knowledge I/MainActivity: onDestroy:

此時我們再新建一個DialogActivity,是通過new-activity-emptyActivity創建的,我們在清單文件中修改如下:

<activity android:name=".DialogActivity" android:theme="@style/Theme.AppCompat.Dialog">

</activity>

從Main中啓動DialogActivity,會出現下圖情況。
dialog
點擊屏幕任意區域,DialogActvity即退出。過程日誌如下。

27325-27325/com.breezehan.knowledge I/MainActivity: onCreate:
27325-27325/com.breezehan.knowledge I/MainActivity: onStart:
27325-27325/com.breezehan.knowledge I/MainActivity: onResume:
27325-27325/com.breezehan.knowledge I/MainActivity: onPause:
27325-27325/com.breezehan.knowledge I/MainActivity: onResume:

說明此種情況並不是完全可見,所以Main只會onPause。

5、生命週期的異常情況

上面的官方Activity生命週期圖中所說,當memory不足時會kill掉Activity,導致重新創建。這個我們不容易模擬,但是橫豎屏切換時,也是會導致Activity重新創建的。
我們可以試驗下,進入MainActivity,然後點擊模擬器的屏幕旋轉按鈕。日誌會出現下面的情景。

com.breezehan.knowledge I/MainActivity: onCreate:
com.breezehan.knowledge I/MainActivity: onStart:
com.breezehan.knowledge I/MainActivity: onResume:
com.breezehan.knowledge I/MainActivity: onPause:
com.breezehan.knowledge I/MainActivity: onStop:
com.breezehan.knowledge I/MainActivity: onDestroy:
com.breezehan.knowledge I/MainActivity: onCreate:
com.breezehan.knowledge I/MainActivity: onStart:
com.breezehan.knowledge I/MainActivity: onResume:

再點擊旋轉,會繼續onDestory->onCreate。那麼我們就有疑問了,如果當然頁面用戶已經產生一些交互(編輯、動作、EditText等)。用戶不小心使屏幕自動旋轉了,不能重新填寫處理吧。這裏,Activity爲我們提供了onSaveInstanceState和onRestoreInstanceState(其實onCreate的參數bundle也參與)。注意的情況,像EditText等系統本身已實現了onSaveInstanceState方法,所以不會真的丟失數據。
我們在MainActivity中改造。添加下面的代碼。

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.i(TAG, "onCreate: ");
        if (savedInstanceState != null) {
            Log.i(TAG, "savedInstanceState: "+savedInstanceState.getString("saveState"));
        }
    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        String tempData = "Something is saved.";
        outState.putString("saveState", tempData);
        Log.i(TAG, "onSaveInstanceState: ");
    }

    @Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
        super.onRestoreInstanceState(savedInstanceState);
        Log.i(TAG, "onRestoreInstanceState: ");
    }

此時我們運行成功後,切換屏幕旋轉。會有類似下面的日誌。

com.breezehan.knowledge I/MainActivity: onCreate:
com.breezehan.knowledge I/MainActivity: onStart:
com.breezehan.knowledge I/MainActivity: onResume:
com.breezehan.knowledge I/MainActivity: onPause:
com.breezehan.knowledge I/MainActivity: onSaveInstanceState:
com.breezehan.knowledge I/MainActivity: onStop:
com.breezehan.knowledge I/MainActivity: onDestroy:
com.breezehan.knowledge I/MainActivity: onCreate:
com.breezehan.knowledge I/MainActivity: savedInstanceState: Something is saved.
com.breezehan.knowledge I/MainActivity: onStart:
com.breezehan.knowledge I/MainActivity: onRestoreInstanceState:
com.breezehan.knowledge I/MainActivity: onResume:

這裏我只是旋轉了一下。onSaveInstanceState一定會在onStop之前;onRestoreInstanceState(如果有的話)在onStart之後。如果此時你在Main中啓動Second,會發現onSaveInstanceState方法還是走了,但是返回的時候onRestoreInstanceState並沒有走onRestoreInstanceState,且onCreate中savedInstanceState==null。也就說onRestoreInstanceState的參數值一定不爲空。

6、Activity啓動模式

任務是說一系列Activity的集合,但這些Activity根據各自的打開順序排列在堆棧(即返回棧)中。而返回棧是一種後進先出的數據結構,最後入棧的總是在棧頂。
可以簡單理解爲Activity處於任務棧(返回棧)中。
在清單文件中聲明 Activity 時,可以使用 < activity> 元素的 launchMode 屬性指定 Activity 應該如何與任務關聯。standard、singleTop、singleTask、singleInstance。

6.1 standard
標準模式,系統默認模式。每次都會重新創建活動一個實例,不管這個實例是否存在。誰啓動的這個Activity,那麼這個Activity就會和啓動它的那個Activity在一個任務棧中。
我們創建一個FirstActivity,launchMode不填寫,即默認standard。

public class FirstActivity extends AppCompatActivity {
    private static final String TAG = "FirstActivity";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(TAG, "onCreate: "+this.toString());
        setContentView(R.layout.activity_first);
        ((TextView)findViewById(R.id.textView)).setText(this.toString());
        findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startActivity(new Intent(FirstActivity.this,FirstActivity.class));
            }
        });
    }
}

點擊button多次

FirstActivity: onCreate: com.breezehan.knowledge.launchmode.FirstActivity@cdbda87
FirstActivity: onCreate: com.breezehan.knowledge.launchmode.FirstActivity@3dc837c
FirstActivity: onCreate: com.breezehan.knowledge.launchmode.FirstActivity@714b12d

也就是說A是standard模式,那麼會在一個棧中產生AAA…的情況。

6.2 singleTop
棧頂複用模式。如果要啓動的Activity處於棧頂,則不會重新創建,同時會調用onNewIntent方法。
即如果ABCD四個Activity,A棧低,D棧頂。如果D爲singleTop模式,那麼D中再啓動D,棧內情況依舊是ABCD。但是如果此時啓動C,就會是ABCDC,即使C也是singleTop模式。
6.3 singleTask
棧內複用模式。如果該 Activity 的一個實例已存在於一個單獨的任務中,則系統會通過調用現有實例的 onNewIntent() 方法向其傳送 Intent,而不是創建新實例。否則系統創建新任務並實例化 Activity,位於新任務棧最底部。
有幾種情況。(配合taskAffinity使用,此處不深究,簡單就是指定Activity所處的任務棧名稱)

  • 假如ABC處於S1任務中,這時啓動D(D是singleTask模式,且指定任務棧是S2),由於此時S2和D都不存在,所以會先創建S2,然後在S2中創建D。
  • 假如ABC處於S1任務中,這時啓動D(D是singleTask模式,且指定任務棧是S1),則只是創建D,併入棧S1,ABCD。
  • 假如ADBC處於S1任務中,這時啓動D(D是singleTask模式,且指定任務棧是S1),則只是調用D的onNewIntent方法,將D置於棧頂。但是有clearTop的效果,導致BC出棧,此時S1棧爲AD。

    6.4 singleInstance
    單實例模式。棧中只有一個Activity,處於單獨的任務棧中。如果啓動A,然後啓動B(singleInstance),然後啓動C,此時是兩個棧,當前棧AC,單獨棧中B。如果此時點擊返回鍵,會先到A,因爲兩者處於同一任務棧中,再點返回鍵,纔會到B。

7、Intent的標誌flags

啓動 Activity 時,可以通過在傳遞給 startActivity() 的 Intent 中加入相應的標誌,修改 Activity 與其任務的默認關聯方式。常用如下:

  • FLAG_ACTIVITY_SINGLE_TOP
    此標誌位跟launchMode的singleTop模式效果一致。
  • FLAG_ACTIVITY_NEW_TASK
    官方說與 “singleTask”launchMode 值相同的行爲。你要相信嗎,稍等驗證。
  • FLAG_ACTIVITY_CLEAR_TOP
    使用此標誌啓動時,會使在同一任務棧中,把位於當前Activity上面的所有Activity出棧,銷燬。
    沒有對應的launchMode 。一般會和FLAG_ACTIVITY_NEW_TASK同時使用。

我們來驗證下FLAG_ACTIVITY_NEW_TASK行爲。
改造剛纔的FirstActivity。

    Intent intent = new Intent(FirstActivity.this,FirstActivity.class);
    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    startActivity(intent);
    ...
    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        Log.d(TAG, "onNewIntent: ");
    }

多次點擊button。

com.breezehan.knowledge D/FirstActivity: onCreate: com.breezehan.knowledge.launchmode.FirstActivity@5ded4f5
com.breezehan.knowledge D/FirstActivity: onCreate: com.breezehan.knowledge.launchmode.FirstActivity@bfc3e92
com.breezehan.knowledge D/FirstActivity: onCreate: com.breezehan.knowledge.launchmode.FirstActivity@8113ecb

不對啊,爲什麼每次都創建新的實例了,不是說和singleTask一樣嗎?!
我們再寫一個OtherActivity,能啓動FirstActivity。
FirstActivity中

public class FirstActivity extends AppCompatActivity {
    private static final String TAG = "FirstActivity";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(TAG, "onCreate: "+this.toString()+"\ttaskId:"+getTaskId());
        setContentView(R.layout.activity_first);
        ((TextView)findViewById(R.id.textView)).setText(this.toString());
        findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(FirstActivity.this,OtherActivity.class);
                startActivity(intent);
            }
        });
    }

    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        Log.d(TAG, "onNewIntent: ");
    }
}

OtherActivity如下

public class OtherActivity extends AppCompatActivity {
    private static final String TAG = "OtherActivity";
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.d(TAG, "onCreate: "+this.toString()+"\ttaskId:"+getTaskId());
        setContentView(R.layout.activity_other);
        TextView textView = (TextView) findViewById(R.id.textView);
        textView.setText(this.toString());
        Button button = (Button) findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(OtherActivity.this,FirstActivity.class);
                intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                startActivity(intent);
            }
        });
    }
    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        Log.d(TAG, "onNewIntent: ");
    }
}

順序first->other->first

FirstActivity: onCreate: com.breezehan.knowledge.launchmode.FirstActivity@18b9f2b taskId:249
OtherActivity: onCreate: com.breezehan.knowledge.launchmode.OtherActivity@f41828a taskId:249
FirstActivity: onCreate: com.breezehan.knowledge.launchmode.FirstActivity@3059cf4 taskId:249

What?什麼情況,完全沒反應嗎?即使我們執行adb shell dumpsys activity也是如此。

Running activities (most recent first):
TaskRecord{6946e03 #249 A=com.breezehan.knowledge U=0 StackId=1 sz=3}
Run #2: ActivityRecord{ba9939e u0 com.breezehan.knowledge/.launchmode.FirstActivity t249}
Run #1: ActivityRecord{9b4d225 u0 com.breezehan.knowledge/.launchmode.OtherActivity t249}
Run #0: ActivityRecord{9444454 u0 com.breezehan.knowledge/.launchmode.FirstActivity t249}

那麼我們再OtherActivity改造如下:

 button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        Intent intent = new Intent(OtherActivity.this,FirstActivity.class);
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK|Intent.FLAG_ACTIVITY_CLEAR_TOP);
        startActivity(intent);
    }
});

最後打印的日誌還是跟上面差不多。但是在first->other->first兩次點擊後,執行adb shell dumpsys activity。

ACTIVITY MANAGER ACTIVITIES (dumpsys activity activities)
Display #0 (activities from top to bottom):
Stack #1:
mFullscreen=true
mBounds=null
Task id #255
mFullscreen=true
mBounds=null
mMinWidth=-1
mMinHeight=-1
mLastNonFullscreenBounds=null
TaskRecord{3032265 #255 A=com.breezehan.knowledge U=0 StackId=1 sz=1}
Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10000000 cmp=com.breezehan.knowledge/.launchmode.FirstActivity }
Hist #0: ActivityRecord{1f342fc u0 com.breezehan.knowledge/.launchmode.FirstActivity t255}
Intent { flg=0x14400000 cmp=com.breezehan.knowledge/.launchmode.FirstActivity }
ProcessRecord{e43ef5c 17311:com.breezehan.knowledge/u0a94}

Running activities (most recent first):
TaskRecord{3032265 #255 A=com.breezehan.knowledge U=0 StackId=1 sz=1}
Run #0: ActivityRecord{1f342fc u0 com.breezehan.knowledge/.launchmode.FirstActivity t255}

此時taskRecord中只有一個FirstActivity,點擊一次返回鍵,直接退出app。

8、處理關聯 taskAffinity

“關聯”指示 Activity 優先屬於哪個任務。默認情況下,同一應用中的所有 Activity 彼此關聯。 因此,默認情況下,同一應用中的所有 Activity 優先位於相同任務棧中。 不過,您可以修改 Activity 的默認關聯。 在不同應用中定義的 Activity 可以共享關聯,或者可爲在同一應用中定義的 Activity 分配不同的任務關聯。
可以使用 < activity> 元素的 taskAffinity 屬性修改任何給定 Activity 的關聯。
taskAffinity 屬性取字符串值,該值必須不同於在 < manifest> 元素中聲明的默認軟件包名稱,因爲系統使用該名稱標識應用的默認任務關聯。
白話就是,你不設置這個選項,默認是跟啓動自己的activity處於同一任務棧中,如果設置了taskAffinity 的值且不跟包名相同,待啓動的Activity會處於名字和taskAffinity相同的任務棧中 。當然要跟下面的情況一起使用哦,單獨無效。
在兩種情況下,關聯會起作用:

  • 啓動 Activity 的 Intent 包含 FLAG_ACTIVITY_NEW_TASK 標誌(此處等同設置singleTask)。
    此時,會開啓一個新任務(如果不存在taskAffinity中設置的任務),在任務中啓動Activity。但是如果已存在taskAffinity指定的任務,則在關聯的任務中啓動Activity。如下OtherActivity的清單文件中,設置taskAffinity=”com.breezehan.knowledge.other”。first->other運行,啓動了新的任務棧。

Running activities (most recent first):
TaskRecord{a0dac3a #258 A=com.breezehan.knowledge.other U=0 StackId=1 sz=1}
Run #1: ActivityRecord{4e7230e u0 com.breezehan.knowledge/.launchmode.OtherActivity t258}
TaskRecord{12830eb #257 A=com.breezehan.knowledge U=0 StackId=1 sz=1}
Run #0: ActivityRecord{8fd516b u0 com.breezehan.knowledge/.launchmode.FirstActivity t257}

  • Activity 將其 allowTaskReparenting 屬性設置爲 “true”。
    在這種情況下,Activity 可以從其啓動的任務移動到與其具有關聯的任務(如果該任務出現在前臺)。
    例子:應用A,啓動應用B中的ActvitityC(清單文件中allowTaskReparenting = true)。然後”HOME”鍵回到主頁,打開應用B,此時不會啓動B中的MainActivity,而是直接顯示的ActivityC。因爲兩者包名不一,默認taskAffinity不一樣。

    白話版:
    1、如果上述設置,應用A中ActvitityA啓動應用B的ActvitityC,此時ActvitityA、ActvitityC同處於應用A的任務棧中。
    2、此時HOME後,啓動應用B,則會直接展現ActvitityC,此時ActvitityC跑到了應用B的任務棧中。
    你看allowTaskReparenting 就是會重定位所在任務棧。

上述的FirstActivity改造如下。

Intent intent = new Intent();
intent.setAction("com.breezehan.taskaffinity.reparent");
startActivity(intent);

新建一個Module,除了MainActivity,新建一個ReparentActivity。

<activity android:name=".ReparentActivity"
    android:allowTaskReparenting="true">
    <intent-filter>
        <action android:name="com.breezehan.taskaffinity.reparent" />
        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
</activity>

安裝兩個應用,在FirstActivity中啓動ReparentActivity,然後HOME,啓動TaskAffinity應用,發現確實是顯示的ReparentActivity。
FirstActivity->ReparentActivity時,ReparentActivity跟FirstActivity同一棧中。

Running activities (most recent first):
TaskRecord{4a3f11c #288 A=com.breezehan.knowledge U=0 StackId=1 sz=2}
Run #1: ActivityRecord{66421bf u0 com.breezehan.taskaffinity/.ReparentActivity t288}
Run #0: ActivityRecord{2765673 u0 com.breezehan.knowledge/.launchmode.FirstActivity t288}

mResumedActivity: ActivityRecord{66421bf u0 com.breezehan.taskaffinity/.ReparentActivity t288}

FirstActivity點擊HOME鍵後,啓動TaskAffinity,ReparentActivity移動到自己的棧中了。

Running activities (most recent first):
TaskRecord{30ce4f #289 A=com.breezehan.taskaffinity U=0 StackId=1 sz=2}
Run #1: ActivityRecord{66421bf u0 com.breezehan.taskaffinity/.ReparentActivity t289}
TaskRecord{4a3f11c #288 A=com.breezehan.knowledge U=0 StackId=1 sz=1}
Run #0: ActivityRecord{2765673 u0 com.breezehan.knowledge/.launchmode.FirstActivity t288}
mResumedActivity: ActivityRecord{66421bf u0 com.breezehan.taskaffinity/.ReparentActivity t289}

詳細Demo請到我的githubAndroidKnowledgeSummary中查看,app和taskAffinity兩個module來展現。

9、清理返回棧

下方爲Android官方API指南的內容

如果用戶長時間離開任務,則系統會清除所有 Activity 的任務,根 Activity 除外。 當用戶再次返回到任務時,僅恢復根 Activity。系統這樣做的原因是,經過很長一段時間後,用戶可能已經放棄之前執行的操作,返回到任務是要開始執行新的操作。

您可以使用下列幾個 Activity 屬性修改此行爲:

  • alwaysRetainTaskState
    如果在任務的根 Activity 中將此屬性設置爲 “true”,則不會發生剛纔所述的默認行爲。即使在很長一段時間後,任務仍將所有 Activity 保留在其堆棧中。
  • clearTaskOnLaunch
    如果在任務的根 Activity 中將此屬性設置爲 “true”,則每當用戶離開任務然後返回時,系統都會將堆棧清除到只剩下根 Activity。 換而言之,它與 alwaysRetainTaskState 正好相反。 即使只離開任務片刻時間,用戶也始終會返回到任務的初始狀態。
  • finishOnTaskLaunch
    此屬性類似於 clearTaskOnLaunch,但它對單個 Activity 起作用,而非整個任務。 此外,它還有可能會導致任何 Activity 停止,包括根 Activity。 設置爲 “true” 時,Activity 仍是任務的一部分,但是僅限於當前會話。如果用戶離開然後返回任務,則任務將不復存在。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章