IPC相關(guān)概念
IPC是Inter-Process-Communication的縮寫,即進(jìn)程間通信。Binder是Android中最具特色的進(jìn)程間通信方式。
- 多進(jìn)程引用方式
Android中引用多進(jìn)程的唯一方式:對四大組件在AndroidMenifest.xml中指定android:process屬性。
<activity
android:name=".ui.activity.TestActvity"
android:screenOrientation="portrait"
android:process=":test"
android:windowSoftInputMode="stateAlwaysHidden|adjustPan"/>
沒有指定android:process屬性就是默認(rèn)進(jìn)程,進(jìn)程名為包名。
兩種進(jìn)程名寫法:
android:process=": test",該進(jìn)程名前附上包名即為其完整進(jìn)程名,即com.zjrb.sjzsw: test。表示該進(jìn)程為應(yīng)用私有,不可共享。
android:process="com.zjrb.sjzsw.test",該名即為完整進(jìn)程名,屬于全局進(jìn)程,其他組件可通過ShareUID方式共享進(jìn)程。
- 序列化和反序列化
Serializable是Java提供的標(biāo)準(zhǔn)序列化接口,可在類的聲明中指定標(biāo)識(靜態(tài)常量)用于反序列化時(shí)標(biāo)識是同一個(gè)數(shù)據(jù)源。
//推薦手動(dòng)指定serialVersionUID值,避免類結(jié)構(gòu)改變引起hash值改變,進(jìn)而反序列化失敗。
private static final long serialVersionUID = 8711368828010083044L;
序列化和反序列化的過程即為將對象實(shí)例的數(shù)據(jù)寫入文件和從文件讀取的過程。原理同下:

靜態(tài)變量屬于類不屬于對象實(shí)例,不參與序列化過程;用transient關(guān)鍵字標(biāo)記的變量不參與序列化過程。
Parcelable和Serializable的區(qū)別:
- Serializable是Java中的序列化接口,其使用起來簡單但是開銷很大,序列化和反序列化過程中需要大量的I/O操作。
- Parcelable是Android中的序列化接口,更適合運(yùn)用在Android平臺上,缺點(diǎn)是使用起來麻煩些,但效率高。
Parcelable主要運(yùn)用在內(nèi)存序列化上,Serializable將對象序列化到存儲設(shè)備中或者將序列化后通過網(wǎng)絡(luò)傳輸比較方便。
在AIDL進(jìn)程間通信中,實(shí)體類序列化宜采用Parcelable,實(shí)測暫不支持Serializable方式的序列化。
Binder機(jī)制
- 進(jìn)程隔離
進(jìn)程之間無法直接進(jìn)行交互的,操作系統(tǒng)為了保證自身的安全穩(wěn)定性,將系統(tǒng)空間分為內(nèi)核空間和用戶空間。
內(nèi)核空間是系統(tǒng)內(nèi)核運(yùn)行的空間,內(nèi)核空間數(shù)據(jù)可共享。
用戶空間是用戶程序運(yùn)行的空間,用戶空間數(shù)據(jù)不共享。因此進(jìn)程間通信是靠內(nèi)核空間驅(qū)動(dòng)的。

當(dāng) Client 向 Server 發(fā)起 IPC 請求時(shí),Client 會先將請求數(shù)據(jù)從用戶空間拷貝到內(nèi)核空間。系統(tǒng)會將內(nèi)核空間中的數(shù)據(jù)拷貝到 Server端用戶空間的緩存中。這樣就成功的將 Client 進(jìn)程中的請求數(shù)據(jù)傳遞到了 Server 進(jìn)程中。
- Binder內(nèi)在原理
Binder是一種架構(gòu),提供了服務(wù)端接口、Binder驅(qū)動(dòng)、客戶端接口三個(gè)模塊。
ServiceManager:是一個(gè)獨(dú)立的進(jìn)程,管理各種系統(tǒng)服務(wù)??蛻舳苏{(diào)用Service之前,會向ServiceManager查詢該服務(wù)是否存在,若存在則返回該Service的引用。
Binder描述符:在Binder內(nèi)部,其唯一標(biāo)識由當(dāng)前Binder的類名全路徑表示。
onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags):
該方法運(yùn)行在服務(wù)端的Binder線程池中,當(dāng)客戶端發(fā)起跨進(jìn)程請求時(shí),遠(yuǎn)程請求會在內(nèi)核空間的驅(qū)動(dòng)交由此方法來處理。
- code 服務(wù)端通過code確定客戶端請求的目標(biāo)方法;
- data 從data中取出目標(biāo)方法所需的參數(shù),如果有的話;
- reply 目標(biāo)方法執(zhí)行完畢后,會向reply中寫入返回值,如果有的話;
- flags 表示是否需要阻塞等待返回結(jié)果,0或FLAG_ONEWAY。
Binder通信流程如下:

服務(wù)端Binder對象創(chuàng)建后(同時(shí)Binder驅(qū)動(dòng)中也會創(chuàng)建一個(gè)Binder對象),內(nèi)部會啟動(dòng)一個(gè)隱藏的線程,該線程會接收Binder驅(qū)動(dòng)發(fā)送的消息,之后會執(zhí)行Binder對象的onTransact()函數(shù),并按照該函數(shù)的參數(shù)執(zhí)行不同的服務(wù)代碼。
客戶端通過Binder驅(qū)動(dòng)中的Binder對象,該對象名為mRemote,調(diào)用其transact()方法,向服務(wù)端發(fā)送消息,并掛起客戶端當(dāng)前線程,待接收到服務(wù)端執(zhí)行完指定函數(shù)后的通知,客戶端線程恢復(fù)喚醒狀態(tài)。
為什么用Binder機(jī)制?
- 性能:Binder機(jī)制下數(shù)據(jù)拷貝只需一次,而管道、消息隊(duì)列和socket都需要兩次,共享內(nèi)存不需要內(nèi)存拷貝,略優(yōu)于Binder。
- 穩(wěn)定性:Binder基于C/S架構(gòu),雙端獨(dú)立解耦,而共享內(nèi)存需要處理并發(fā)問題。
- 安全性:Android為每個(gè)應(yīng)用程序分配了鑒別進(jìn)程身份的UID,C端將指令發(fā)送S端,S端會根據(jù)權(quán)限控制執(zhí)行策略。
進(jìn)程間通信方式
Bundle
Bundle實(shí)現(xiàn)了Parcelable接口,支持activity/service/receiver在進(jìn)程間傳遞數(shù)據(jù)(ContentProvider默認(rèn)支持進(jìn)程間通信),并通過Intent發(fā)送。
Bundle支持的數(shù)據(jù)類型就是進(jìn)程間通信的數(shù)據(jù)類型,即數(shù)據(jù)實(shí)現(xiàn)序列化。
最簡單的進(jìn)程間數(shù)據(jù)傳遞方式,推薦。
文件共享
兩個(gè)進(jìn)程通過讀寫同一個(gè)文件來交換數(shù)據(jù),需要處理好線程同步問題,避免并發(fā)沖突。
SharedPreferences是采用XML文件來存儲鍵值對,是帶有緩存的文件存儲,不推薦在進(jìn)程間傳遞數(shù)據(jù)。
適合對同步要求不高的場景。
Messenger
Messenger底層是通過封裝AIDL實(shí)現(xiàn)進(jìn)程間通信,通過帶有Handler的客戶端接收從服務(wù)端傳回的消息。
優(yōu)點(diǎn):支持一對多通信;支持實(shí)時(shí)通信;無并發(fā)問題;
缺點(diǎn):不支持RPC(遠(yuǎn)程過程調(diào)用);數(shù)據(jù)類型單一(僅支持Bundle支持的數(shù)據(jù)類型);
簡單的消息傳輸,不支持RPC,適合于低并發(fā)的一對多即時(shí)通信的場景。
- 案例解析:客戶端client給服務(wù)端發(fā)消息,服務(wù)端server收到消息后回復(fù)客戶端。
客戶端:
public class MessengerActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.ok).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent("com.jinzifu.myserver.Messenger");
intent.setPackage("com.jinzifu.myserver");
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
}
});
}
ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
//① 建立通信 客戶端通過Messenger向服務(wù)端發(fā)消息
Messenger messenger = new Messenger(service);
Bundle bundle = new Bundle();
bundle.putString("fromClient", "這是客戶端對你家人的問候。");
Message message = new Message();
message.what = 102;
message.setData(bundle);
//② 傳遞客戶端Messenger,用于接收服務(wù)端傳回的消息
message.replyTo = mMessenger;
try {
messenger.send(message);
} catch (RemoteException e) {
e.printStackTrace();
}
}
@Override
public void onServiceDisconnected(ComponentName name) {
}
};
Messenger mMessenger = new Messenger(new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case 101:
Bundle bundle = msg.getData();
String content = bundle.getString("fromServer");
Log.d("jinzifu", "來自服務(wù)端的消息:" + content);
break;
}
}
});
}
- 建立通信 客戶端通過Messenger向服務(wù)端發(fā)消息;
- 向服務(wù)端傳遞客戶端帶有Handler的Messenger,用于接收服務(wù)端傳回的消息;
服務(wù)端:
<service android:name=".MessengerService">
<intent-filter>
<action android:name="com.jinzifu.myserver.Messenger" />
</intent-filter>
</service>
public class MessengerService extends Service {
@Nullable
@Override
public IBinder onBind(Intent intent) {
return new Messenger(new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case 102:
Bundle bundle = msg.getData();
String content = bundle.getString("fromClient");
Log.d("jinzifu", "來自客戶端的消息:" + content);
Messenger messenger = msg.replyTo;
Bundle bundle1 = new Bundle();
bundle1.putString("fromServer", "已收到,回敬與你。");
Message message = new Message();
message.what = 101;
message.setData(bundle1);
try {
//③ 獲得客戶端的Messenger,并回傳消息
messenger.send(message);
} catch (RemoteException e) {
e.printStackTrace();
}
break;
}
}
}).getBinder();
}
}
- 獲得客戶端的Messenger,并回傳消息;
Log日志
com.jinzifu.myserver D/jinzifu: 來自客戶端的消息:這是客戶端對你家人的問候。
com.jinzifu.myclient D/jinzifu: 來自服務(wù)端的消息:已收到,回敬與你。
AIDL
AIDL 即Android Interface definition language的縮寫,是Android接口定義語言。是系統(tǒng)內(nèi)部提供的一種快速實(shí)現(xiàn)Binder的工具而已,也可手動(dòng)實(shí)現(xiàn)。在Android中常用的通信方式是基于AIDL的遠(yuǎn)程Service。
基于AIDL的遠(yuǎn)程Service是非常復(fù)雜的通信方式,需考慮線程阻塞、權(quán)限驗(yàn)證、死亡監(jiān)聽等。
AIDL支持的所有類型(未列類型暫不支持):

注意:
- AIDL中傳遞的接口,不是普通的接口,必須是AIDL接口。且接口中只支持方法,不支持聲明靜態(tài)常量。
- AIDL中自定義的Parcelable對象和AIDL對象必須要顯式的import進(jìn)來,不管是否在同一個(gè)包內(nèi)。
- AIDL中除了基本數(shù)據(jù)類型,其他支持的類型的參數(shù)都要標(biāo)注方向:in、out和inout。
- 所有的AIDL接口都繼承于 android.os.IInterface接口。
interface BaseDataAidlInterface {
/**
* 類型示例,刪除即可
*/
void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
double aDouble, String aString,in List<String> list);
}
in、out和inout的區(qū)別:
- in表示輸入型參數(shù),數(shù)據(jù)只能由客戶端傳向服務(wù)端,服務(wù)端對數(shù)據(jù)的修改不會影響到客戶端。
- out表示輸出型參數(shù),數(shù)據(jù)只能從服務(wù)端傳向客戶端,即使客戶端通過方法入?yún)⑾蚍?wù)端傳入對象,值也為空的。
- inout表示輸入輸出型參數(shù)。
in/out標(biāo)簽允許Binder跳過編組(序列化、傳輸、接收和反序列化)步驟,以獲得更好的性能。
ContentProvider
ContentProvider是Android提供的專門為不同應(yīng)用進(jìn)行數(shù)據(jù)共享的方式。主要以表格的形式組織數(shù)據(jù),還支持圖片、視頻等文件數(shù)據(jù)。
優(yōu)點(diǎn):在數(shù)據(jù)源訪問方面功能強(qiáng)大,支持一對多并發(fā)數(shù)據(jù)共享,可通過Call方法擴(kuò)展其他操作。
缺點(diǎn):可以理解為受約束的AIDL,主要提供數(shù)據(jù)源的CRUD(create read update delete)操作,要注意SQLite注入和目錄遍歷的漏洞。
Android封裝好的進(jìn)程間數(shù)據(jù)共享方式,推薦,不支持RPC。
- 案例解析:服務(wù)端操作數(shù)據(jù)庫提供數(shù)據(jù)給客戶端。
URI統(tǒng)一資源標(biāo)識符:是標(biāo)識ContentProvider中資源唯一性的符號。
Uri uri = Uri.parse("content://com.jzf.progress/book/1");
- content:主題名,是ContentProvider的URI前綴,系統(tǒng)規(guī)定的;
- com.jzf.progress:授權(quán)信息,ContentProvider的唯一標(biāo)識符;
- book:表名,ContentProvider指向數(shù)據(jù)庫中的具體表名;
- 1:記錄,表中的某個(gè)記錄,如果沒指定,默認(rèn)返回的是全部記錄;
Android提供了用于操作Uri的工具類UriMatcher,使用UriMatcher.match(uri)對輸入的Uri進(jìn)行匹配,如果匹配成功就返回匹配碼,匹配碼是調(diào)用addURI()方法傳入的第三個(gè)參數(shù)。
ContentResolver 內(nèi)容解析器:ContentResolver提供了與ContentProvider一樣的增刪改查方法,統(tǒng)一管理不同ContentProvider與外部進(jìn)程的通信。ContentProvider#notifyChange方法通知外界訪問者ContentProvider中的數(shù)據(jù)有更新。
服務(wù)端
public class EventProvider extends ContentProvider {
private static final String AUTHORITY = "com.jinzifu.myserver.EventProvider";
//① 關(guān)聯(lián)Uri和Uri_Code,識別外界要操作的表
private static final int EVENT_URI_CODE = 101;
private static final UriMatcher uriMathcher =
new UriMatcher(UriMatcher.NO_MATCH);
private static final String TAG = "jinzifu";
static {
uriMathcher.addURI(AUTHORITY, "event", EVENT_URI_CODE);
}
private Context mContext;
private SQLiteDatabase mSQLiteDatabase;
/**
* ① 一般用于創(chuàng)建數(shù)據(jù)庫或升級等操作,外界調(diào)用getContentResolver()時(shí)回調(diào)。
* onCreate 運(yùn)行在UI線程中,其他方法運(yùn)行在binder線程中。
*
* @return fasle則表示provider創(chuàng)建失敗
*/
@Override
public boolean onCreate() {
Log.d(TAG, "onCreate: EventProvider初始化");
mContext = getContext();
mSQLiteDatabase = new MySQLiteHelper(mContext, "",
null, 0).getWritableDatabase();
new Thread(new Runnable() {
@Override
public void run() {
//② 數(shù)據(jù)庫操作不應(yīng)該放在主線程,數(shù)據(jù)庫操作命令需掌握
mSQLiteDatabase.execSQL("delete from "
+ MySQLiteHelper.TABLE_EVENT);
mSQLiteDatabase.execSQL("insert into "
+ MySQLiteHelper.TABLE_EVENT
+ " values(1,'jzf',100);");
}
}).start();
return true;
}
/**
* ⑤ 查詢數(shù)據(jù)
*
* @param uri 根據(jù)uri查詢數(shù)據(jù)庫中對應(yīng)表的數(shù)據(jù)
* @param projection 選擇符合條件的列查詢數(shù)據(jù),傳null則查詢所有列
* @param selection 選擇符合條件的行查詢數(shù)據(jù),傳null則查詢所有行
* @param selectionArgs 類似selection
* @param sortOrder 對查詢結(jié)果排序,傳null則為默認(rèn)排序,也可無序
* @return 返回一個(gè)Cursor對象,用后須關(guān)閉,避免內(nèi)存泄露
*/
@Nullable
@Override
public Cursor query(@NonNull Uri uri,
@Nullable String[] projection,
@Nullable String selection,
@Nullable String[] selectionArgs,
@Nullable String sortOrder) {
String table = getTableFormUri(uri);
if (TextUtils.isEmpty(table)) return null;
Log.d("jinzifu", "EventProvider開始查詢");
return mSQLiteDatabase.query(table,
projection,
selection,
selectionArgs,
null,
null,
sortOrder);
}
private String getTableFormUri(Uri uri) {
switch (uriMathcher.match(uri)) {
case EVENT_URI_CODE:
return MySQLiteHelper.TABLE_EVENT;
}
return null;
}
/**
* 返回指定內(nèi)容的媒體類型
*
* @param uri
* @return MIME類型 如圖片、視頻等,可為null
*/
@Nullable
@Override
public String getType(@NonNull Uri uri) {
return null;
}
/**
* ② 添加數(shù)據(jù)
*
* @param uri 根據(jù)uri插入數(shù)據(jù)庫中對應(yīng)表的數(shù)據(jù)
* @param values ContentValues內(nèi)部使用HashMap存儲數(shù)據(jù)的,
* key表示列名,value表示行名,如果value為空,在表中是空行,無內(nèi)容。
* @return 返回這條數(shù)據(jù)的uri
*/
@Nullable
@Override
public Uri insert(@NonNull Uri uri,
@Nullable ContentValues values) {
//② 與其他方法均屬于并發(fā)編程的,要做好線程同步??
//③ SQLiteDatabase內(nèi)部對數(shù)據(jù)庫的操作是有同步處理的,
// 但多個(gè)SQLiteDatabase對象對ConentProvider并發(fā)操作無同步處理。
String table = getTableFormUri(uri);
if (TextUtils.isEmpty(table)) return null;
mSQLiteDatabase.insert(table, null, values);
mContext.getContentResolver().notifyChange(uri, null);
Log.d(TAG, "insert: 插入數(shù)據(jù)成功");
return uri;
}
/**
* ③ 刪除數(shù)據(jù)
*
* @param uri 根據(jù)uri刪除數(shù)據(jù)庫中對應(yīng)表的數(shù)據(jù)
* @param selection 選擇符合條件的行數(shù)據(jù)刪除
* @param selectionArgs 類似selection
* @return 返回被刪除的行數(shù)
*/
@Override
public int delete(@NonNull Uri uri,
@Nullable String selection,
@Nullable String[] selectionArgs) {
String table = getTableFormUri(uri);
if (TextUtils.isEmpty(table)) return 0;
int count = mSQLiteDatabase.delete(table,
selection,
selectionArgs);
if (count > 0) mContext.getContentResolver().notifyChange(uri, null);
return count;
}
/**
* ④ 更改數(shù)據(jù)
*
* @param uri 根據(jù)uri修改數(shù)據(jù)庫中對應(yīng)表的數(shù)據(jù)
* @param values 同insert中的ContentValues用法,若value為空,則會將原來的數(shù)據(jù)置空
* @param selection 選擇符合條件的行數(shù)據(jù)修改
* @param selectionArgs 類似selection
* @return 返回更新的行數(shù)
*/
@Override
public int update(@NonNull Uri uri,
@Nullable ContentValues values,
@Nullable String selection,
@Nullable String[] selectionArgs) {
String table = getTableFormUri(uri);
if (TextUtils.isEmpty(table)) return 0;
int row = mSQLiteDatabase.update(table, values, selection, selectionArgs);
if (row > 0) mContext.getContentResolver().notifyChange(uri, null);
return row;
}
}
public class MySQLiteHelper extends SQLiteOpenHelper {
private static final String DB_NAME = "provider_test.db";
private static final int DB_VERSION = 1;
public static final String TABLE_EVENT = "event";
public static final String CREATE_EVENT_TABLE =
"CREATE TABLE IF NOT EXISTS " + TABLE_EVENT
+ "(_id INTEGER PRIMARY KEY,name TEXT,count INTEGER)";
public MySQLiteHelper(@Nullable Context context,
@Nullable String name,
@Nullable SQLiteDatabase.CursorFactory factory,
int version) {
super(context, DB_NAME, null, DB_VERSION);
}
@Override
public void onCreate(SQLiteDatabase db) {
db.execSQL(CREATE_EVENT_TABLE);
}
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
}
<permission android:name="com.jinzifu.myserver.permission.ACCESS_EVENT_PROVIDER"
android:protectionLevel="normal"/>
<!--⑤ android:authorities=""ContentProvider的唯一標(biāo)識,-->
<!--android:writePermission="" 寫權(quán)限,-->
<!--android:readPermission=""讀權(quán)限,若讀寫權(quán)限都要求,則上面都要聲明-->
<!--android:permission=""全部權(quán)限-->
<!--android:exported="true"供外部程序調(diào)用-->
<provider
android:name=".EventProvider"
android:authorities="com.jinzifu.myserver.EventProvider"
android:exported="true"
android:permission="com.jinzifu.myserver.permission.ACCESS_EVENT_PROVIDER" />
客戶端
public class ProviderActivity extends AppCompatActivity {
private static final String TAG = "jinzifu";
private Uri uri =
Uri.parse("content://com.jinzifu.myserver.EventProvider/event");
private ContentObserver mContentObserver;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.ok)
.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
ContentValues contentValues = new ContentValues();
//① 數(shù)據(jù)插入時(shí)就知道數(shù)據(jù)的列名,這是SQLite注入的漏洞點(diǎn)
contentValues.put("_id", 2);
contentValues.put("name", "jzf2");
contentValues.put("count", 101);
getContentResolver().insert(uri, contentValues);
}
});
//監(jiān)聽ContentProvider的數(shù)據(jù)變化回調(diào)
getContentResolver().registerContentObserver(
uri,
true,
mContentObserver = new ContentObserver(new Handler()) {
@Override
public void onChange(boolean selfChange, Uri uri) {
super.onChange(selfChange, uri);
Log.d(TAG, "onChange: 監(jiān)聽到provider數(shù)據(jù)變化");
Cursor cursor = getContentResolver().query(
uri,
new String[]{"name"},
null,
null,
null);
if (cursor == null) return;
while (cursor.moveToNext()) {
Log.d(TAG, "name: "
+ cursor.getString(
cursor.getColumnIndex("name")));
}
//① Cursor用后需要及時(shí)回收,Cursor.close()。
cursor.close();
}
});
}
@Override
protected void onDestroy() {
super.onDestroy();
getContentResolver()
.unregisterContentObserver(mContentObserver);
}
}
Log日志
com.jinzifu.myserver D/jinzifu: insert: 插入數(shù)據(jù)成功
com.jinzifu.myclient D/jinzifu: onChange: 監(jiān)聽到provider數(shù)據(jù)變化
com.jinzifu.myserver D/jinzifu: EventProvider開始查詢
com.jinzifu.myclient D/jinzifu: name: jzf
com.jinzifu.myclient D/jinzifu: name: jzf2
Socket
Socket稱為套接字,分為TCP協(xié)議的流式套接字和UDP協(xié)議的用戶數(shù)據(jù)報(bào)套接字。進(jìn)程間可以通過Socket實(shí)現(xiàn)端到端的通信,且Socket支持任意字節(jié)流。
優(yōu)點(diǎn):功能強(qiáng)大,可通過網(wǎng)絡(luò)傳輸字節(jié)流,支持一對多并發(fā)實(shí)時(shí)通信。
缺點(diǎn):實(shí)現(xiàn)細(xì)節(jié)稍微有點(diǎn)繁瑣,不支持直接的RPC,適用于網(wǎng)絡(luò)數(shù)據(jù)交換(即需要網(wǎng)絡(luò)支持)。