0x10 Intent 組件消息傳遞
Intent是一個重要的類,用於Android組件之間的消息傳遞。我相信你會在任何一個正常的Android應用中發現它的足跡。
Intent是Android程序中各組件之間進行交互的一種重要方式,它不僅可以指明當前組件想要執行的動作,還可以在不同組件之間傳遞數據。Intent一般可被用於啓動活動、啓動服務以及發送廣播等場景
Intent大致可以分爲兩種:顯式Intent 和隱式Intent。
0x11 顯式Intent
顯式Intent,也就是很明顯的使用Intent告訴系統我想啓動哪個組件。新建一個項目,命名爲SendIntent,在MainActivity輸入以下代碼。
button.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View view) {
Intent intent = new Intent(MainActivity.this, SecondActivity.class);
startActivity(intent);
}
});
Intent的構造方法有很多重載,這裏,我們用的是其中的一個構造方法,第一個參數是當前活動,第二個參數是想要拉起的Activity。
再建立一個空的Activity,命名爲SecondActivity,這樣就是用第一個Activity顯示的啓動第二個Activity。比較簡單,不再贅述。
0x12 隱式Intent
實際的項目當中,較少使用這種顯式Intent傳遞,我們更多的可能會遇到隱藏意圖的Intent。這種用法,不會直接說明我們要啓動哪個活動,或者發送消息給哪個組件。
新建一個項目,作爲一個單獨的,要被拉起的App,在其中新建SecondActivity,佈局文件請隨意,也不需要在MainActivity裏面加入過多的,只需要讓我們能夠識別到這是第二個App(TestApplication)中的活動。那麼,怎麼讓別的App識別到這個App中的相關組件呢?最關鍵的就是在Manifest.xml文件中,加入以下代碼
<activity android:name=".SecondActivity" android:exported="true" android:label="@string/title_activity_second" android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.SECOND_START" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.MY_CATEGORY" />
</intent-filter>
</activity>
要想讓該應用的SecondActivity被其他應用識別到,必須加入 android:exported=“true”。action標籤指明瞭當前活動可以響應 android.intent.action.MAIN_START這個action,category標籤是對intent更加細粒度的劃分。每個Intent只能指定一個action,但卻可以指定多個category。
如果想拉起這個應用的該活動,我們需要在自己的應用SendIntent加入以下代碼
button.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View view) {
Intent intent = new Intent("android.intent.action.SECOND_START");
intent.addCategory("android.intent.category.MY_CATEGORY");
startActivity(intent);
}
});
這樣就是一個隱式的Intent傳遞,可使用SendIntent輕鬆拉起我們的TestApplication中的SecondActivity。
0x13 組件間消息傳遞
說了這麼多,Intent怎麼傳遞消息呢?在啓動活動時傳遞數據的思路很簡單,Intent中提供了一系列putExtra() 方法的重載,可以把我們想要傳遞的數據暫存在Intent中,啓動了另一個活動後,只需要把這些數據再從Intent中取出就可以了。也是類似的,我們修改SendIntent的代碼如下
button.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View view) {
Intent intent = new Intent("android.intent.action.SECOND_START");
intent.addCategory("android.intent.category.MY_CATEGORY");
intent.putExtra("TestKey", "我是SendIntent傳遞的數據!");
startActivity(intent);
}
});
再編寫TestApplication中的SecondActivity,添加如下代碼
Intent intent = getIntent();
String data = intent.getStringExtra("TestKey");
Toast.makeText(SecondActivity.this, data, Toast.LENGTH_LONG).show();
這樣就完成了從一個活動向另外一個應用的活動傳遞數據的功能,我們使用的是隱式的方式。
0x20 本應用內傳遞與跨應用傳遞
我們對第一節的內容進行一下總結,可以得到這麼一個結論:如果只是一個應用內的組件之間的消息傳遞,那麼使用顯示的Intent就可以完成,這種方式直接調用Intent(FirstActivity.this, SecondActivity.class)的構造方法就可以完成;如果是跨應用的組件消息傳遞呢?
0x21 跨應用組件消息傳遞
- 方法一:使用我們在0x13節,隱式Intent
- 方法二:使用Component類就可以指定哪個包名下的哪個組件。(相當於顯式Intent)
修改我們的SendIntent如下
button.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View view) {
Intent intent = new Intent();
ComponentName componentName = new ComponentName("com.example.testapplication", "com.example.testapplication.SecondActivity");
intent.setComponent(componentName);
intent.putExtra("TestKey", "我是SendIntent傳遞的數據!");
startActivity(intent);
}
});
運行該應用,達到的效果是和0x13節介紹的一樣。
0x30 使用Parcelable序列化傳遞複雜數據
進行Android開發的時候,無法將對象的引用傳給Activities或者Fragments,我們需要將這些對象放到一個Intent或者Bundle裏面,然後再傳遞。簡單來說就是將對象轉換爲可以傳輸的二進制流(二進制序列)的過程,這樣我們就可以通過序列化,轉化爲可以在網絡傳輸或者保存到本地的流(序列),從而進行傳輸數據 ,那反序列化就是從二進制流(序列)轉化爲對象的過程。
Parcelable是Android爲我們提供的序列化的接口,Parcelable相對於Serializable的使用相對複雜一些,但Parcelable的效率相對Serializable也高很多,這一直是Google工程師引以爲傲的,有時間的可以看一下Parcelable和Serializable的效率對比 Parcelable vs Serializable 號稱快10倍的效率——《簡書:Android中Parcelable的原理和使用方法》
網絡上有關其詳細介紹不勝枚舉,在這裏也不展開來說了。
0x40 Intent與Parcelable暴露組件的安全問題
其實講了這麼多,都是爲了介紹暴露的組件可能引發的一種攻擊,DOS攻擊,當一個組件可被其他應用傳遞消息的時候,需要對接收的Intent進行過濾,不然很容易引發崩潰,我們舉例來說,剛好以實際項目中經常遇到的情況,來說明問題,也剛好說明0x30節,Parcelable帶來的隱藏風險。
0x41 案例分析
以下代碼是一個正常應用的一個組件
package com.lys.testapplication;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.Log;
public class MainActivity extends AppCompatActivity {
private final String TAG = "testapplication";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
a(getIntent());
}
private void a(Intent paramIntent){
if(paramIntent == null){
return;
}
if(paramIntent.getType() != null && "vnd.wfa.wsc".equals(paramIntent.getType())){
Parcelable[] parcelable = paramIntent.getParcelableArrayExtra("android.test.extra.TEST_MESSAGES");
Person b = (Person) parcelable[0];
}else {
Log.d(TAG, "NULL!");
}
}
}
這個MainActivity組件接收Intent,並且使用getType()設置了接收的MIME類型,關於該方法的詳細使用,網上也有例子。總之在發送端使用setType(),就可以設置MIME類型,以匹配getType()。這段代碼是一種常見的接收Parceable序列化對象的寫法,問題的根源在於沒有對異常進行處理,收到構造的Intent,程序會崩潰。那麼怎麼進行構造呢?我們編寫IntentDemo如下
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button = findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
ComponentName componentName = new ComponentName("com.lys.testapplication", "com.lys.testapplication.MainActivity");
Intent intent = new Intent();
intent.setComponent(componentName);
intent.setType("vnd.wfa.wsc");
Person[] person = new Person[0];
intent.putExtra("android.test.extra.TEST_MESSAGES", person);
startActivity(intent);
}
});
}
}
這個應用使用ComponentName類,顯示指定Intent要傳遞的對象,並且使用setType(),設置符合的MIME類型。注意,無論是目標應用com.lys.testapplication,還是我們的IntentDemo.apk,都需要新建一個共有的類,Person(),Parcelable序列化Intent傳遞的就是這個類構造的對象。
public class Person implements Parcelable {
private String name;
private int age;
@Override
public int describeContents(){
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags){
dest.writeString(name);
dest.writeInt(age);
}
public static final Creator<Person> CREATOR = new Creator<Person>() {
@Override
public Person createFromParcel(Parcel source) {
Person person = new Person();
person.name = source.readString();//讀取name
person.age = source.readInt();//讀取age
return person;
}
@Override
public Person[] newArray(int size) {
return new Person[size];
}
};
}
運行我們的IntentDemo.apk,得到如下結果。點擊按鈕,我們觀察彈窗,發現是目標應用TestApplication已經崩潰,說明達到效果。
0x42 原因分析
觀察應用的日誌
取第一行日誌
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.lys.testapplication/com.lys.testapplication.MainActivity}: java.lang.ArrayIndexOutOfBoundsException: length=0; index=0
可以看到是數組越界引發的異常。也就是說,我們使用
Person[] person = new Person[0];
傳遞了一個長度爲0的空數組,但是目標應用並沒有對數組長度進行判斷,就直接引用
Person b = (Person) parcelable[0];
導致出現了數組越界。那麼我們對IntentDemo進行修改,傳遞一個數組長度大於0的對象呢?是不是目標應用TestApplication就不會產生異常了呢?答案是否定的。
0x43 另外一種情況
修改IntentDemo的Person[] person這一行代碼,修改成的代碼如下。這次我們傳遞了一個長度爲1的數組,按照正常情況來說,TestApplication應該就不會崩潰了。
Person[] person = new Person[1];
結果如下,也確實能夠成功拉其該應用,但是我們的目標應用只是個測試案例,接受了person對象,並沒有使用,正常的應用是會使用這個對象的,如果我們在TestApplication代碼中使用person對象,那麼仍然會造成異常。只是這次的異常並非數組越界,而是
我們取第一行的日誌
java.lang.RuntimeException: Unable to start activity ComponentInfo{com.lys.testapplication/com.lys.testapplication.MainActivity}: java.lang.NullPointerException: Attempt to read from null array
儘管person[0]這次沒有越界了,但是元素內容爲空,也會導致空指針異常,這就類似於,我們修改IntentDemo的Person[] person如
String[] person = new String();
可以達到一樣的效果,目標應用並沒有對元素的內容進行檢查,導致應用崩潰。
0x50 總結
無論是使用Intent傳遞簡單的數據,還是使用Parcelable序列化以後的數據,對外部尤其是那些三方組件傳過來的對象,一定要進行異常檢測和數據類型校驗,否則三方應用可能發送一個簡單的Intent就會導致應用崩潰。