Android總結篇系列:Android Service

風靈使發表於2018-11-13

Service通常總是稱之為“後臺服務”,其中“後臺”一詞是相對於前臺而言的,具體是指其本身的執行並不依賴於使用者可視的UI介面,因此,從實際業務需求上來理解,Service的適用場景應該具備以下條件:

1.並不依賴於使用者可視的UI介面(當然,這一條其實也不是絕對的,如前臺Service就是與Notification介面結合使用的);

2.具有較長時間的執行特性。

1.Service AndroidManifest.xml 宣告

一般而言,從Service的啟動方式上,可以將Service分為Started ServiceBound Service。無論哪種具體的Service啟動型別,都是通過繼承Service基類自定義而來。在使用Service時,要想系統能夠找到此自定義Service,無論哪種型別,都需要在AndroidManifest.xml中宣告,語法格式如下:

<service android:enabled=["true" | "false"]
    android:exported=["true" | "false"]
    android:icon="drawable resource"
    android:isolatedProcess=["true" | "false"]
    android:label="string resource"
    android:name="string"
    android:permission="string"
    android:process="string" >
    . . .
</service>

其中,android:exported屬性上一篇博文中對此已進行詳盡描述,android:name對應Service類名,android:permission是許可權宣告,android:process設定具體的程式名稱。需要注意的是Service能否單獨使用一個程式與其啟動方式有關,本後下面會給出具體說明。其他的屬性此處與其他元件基本相同,不再過多描述。

注:如果自定義Service沒有在AndroidManifest.xml中宣告,當具體使用時,不會像Activity那樣直接崩潰報錯,對於顯式Intent啟動的Service,此時也會給出waring資訊“IllegalArgumentException: Service not registered”,有時候不容易發現忘了宣告而一時定位不到問題。

2.Started Service

Started Service相對比較簡單,通過context.startService(Intent serviceIntent)啟動Servicecontext.stopService(Intent serviceIntent)停止此Service。當然,在Service內部,也可以通過stopSelf(...)方式停止其本身。

1)Started Service自定義

下面程式碼片段顯示的是一個最基本的Started Service的自定義方式:

public class MyService extends Service {

    public static final String TAG = "MyService";

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        Log.w(TAG, "in onCreate");
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.w(TAG, "in onStartCommand");
        Log.w(TAG, "MyService:" + this);
        String name = intent.getStringExtra("name");
        Log.w(TAG, "name:" + name);
        return START_STICKY;
    }

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

其中,onBind(...)函式是Service基類中的唯一抽象方法,子類都必須重寫實現,此函式的返回值是針對Bound Service型別的Service才有用的,在Started Service型別中,此函式直接返回 null 即可。onCreate(...)、onStartCommand(...)onDestroy()都是Started Service相應生命週期階段的回撥函式。

2) Started Service使用

public class MainActivity extends Activity {

    public static final String TAG = "MainActivity";

    private Button startServiceBtn;
    private Button stopServideBtn;
    private Button goBtn;

    private Intent serviceIntent;

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

        startServiceBtn = (Button) findViewById(R.id.start_service);
        stopServideBtn = (Button) findViewById(R.id.stop_service);
        goBtn = (Button) findViewById(R.id.go);

        startServiceBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                serviceIntent = new Intent(MainActivity.this, MyService.class);
                startService(serviceIntent);
            }
        });

        stopServideBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                stopService(serviceIntent);
            }
        });

        goBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(MainActivity.this, BActivity.class);
                startActivity(intent);
            }
        });

    }

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

如上程式碼片段,

Client呼叫startService(Intent serviceIntent)後,如果MyService是第一次啟動,首先會執行 onCreate()回撥,然後再執行onStartCommand(Intent intent, int flags, int startId),當Client再次呼叫startService(Intent serviceIntent),將只執行onStartCommand(Intent intent, int flags, int startId),因為此時Service已經建立了,無需執行onCreate()回撥。無論多少次的startService,只需要一次stopService()即可將此Service終止,執行onDestroy()函式(其實很好理解,因為onDestroy()onCreate()回撥是相對的)。

下面重點關注下onStartCommand(Intent intent, int flags, int startId)方法。

其中引數flags預設情況下是0,對應的常量名為START_STICKY_COMPATIBILITY。startId是一個唯一的整型,用於表示此次Client執行startService(…)的請求請求標識,在多次startService(…)的情況下,呈現0,1,2…遞增。另外,此函式具有一個int型的返回值,具體的可選值及含義如下:

  • START_NOT_STICKY:當Service因為記憶體不足而被系統kill後,接下來未來的某個時間內,即使系統記憶體足夠可用,系統也不會嘗試重新建立此Service。除非程式中Client明確再次呼叫startService(…)啟動此Service。
  • START_STICKY:當Service因為記憶體不足而被系統kill後,接下來未來的某個時間內,當系統記憶體足夠可用的情況下,系統將會嘗試重新建立此Service,一旦建立成功後將回撥onStartCommand(…)方法,但其中的Intent將是null,pendingintent除外。
  • START_REDELIVER_INTENT:與START_STICKY唯一不同的是,回撥onStartCommand(…)方法時,其中的Intent將是非空,將是最後一次呼叫startService(…)中的intent。
  • START_STICKY_COMPATIBILITY:compatibility version of {@link #START_STICKY} that does not guarantee that {@link #onStartCommand} will be called again after being killed。此值一般不會使用,所以注意前面三種情形就好。

以上的描述中,”當Service因為記憶體不足而被系統kill後“一定要非常注意,因為此函式的返回值設定只是針對此種情況才有意義的,換言之,當認為的kill掉Service程式,此函式返回值無論怎麼設定,接下來未來的某個時間內,即使系統記憶體足夠可用,Service也不會重啟。

小米手機針對此處做了變更:

另外,需要注意的是,小米手機針對此處做了一定的修改。在“自啟動管理”中有一個自啟動應用列表,預設情況下,只有少應用(如微信、QQ、YY、360等)預設是可以自啟動的,其他應用預設都是禁止的。使用者可以手動新增自啟動應用,新增後的應用中如果Started Service onStartCommand(…)回撥返回值是START_STICKY或START_REDELIVER_INTENT,當使用者在小米手機上長按Home鍵結束App後,接下來未來的某個時間內,當系統記憶體足夠可用時,Service依然可以按照上述規定重啟。當然,如果使用者在 設定 >> 應用 >> 強制kill掉App程式,此時Service是不會重啟的。

注:以上實驗結論基於小米2S親測。

3) Started Service生命週期及程式相關

1.onCreate(Client首次startService(…)) >> onStartCommand >> onStartCommand - optional … >> onDestroy(Client呼叫stopService(…))

注:onStartCommand(…)可以多次被呼叫,onDestroy()onCreate()想匹配,當使用者強制kill掉程式時,onDestroy()是不會執行的。

2.對於同一型別的Service,Service例項一次永遠只存在一個,而不管Client是否是相同的元件,也不管Client是否處於相同的程式中。

3.Service通過startService(…)啟動Service後,此時Service的生命週期與Client本身的什麼週期是沒有任何關係的,只有Client呼叫stopService(…)或Service本身呼叫stopSelf(…)才能停止此Service。當然,當使用者強制kill掉Service程式或系統因記憶體不足也可能kill掉此Service。

4.Client A 通過startService(…)啟動Service後,可以在其他Client(如Client B、Client C)通過呼叫stopService(…)結束此Service。

5.Client呼叫stopService(…)時,如果當前Service沒有啟動,也不會出現任何報錯或問題,也就是說,stopService(…)無需做當前Service是否有效的判斷。

6.startService(Intent serviceIntent),其中的intent既可以是顯式Intent,也可以是隱式Intent,當Client與Service同處於一個App時,一般推薦使用顯示Intent。當處於不同App時,只能使用隱式Intent。

當Service需要執行在單獨的程式中,AndroidManifest.xml宣告時需要通過android:process指明此程式名稱,當此Service需要對其他App開放時,android:exported屬性值需要設定為true(當然,在有intent-filter時預設值就是true)。

<service
    android:name=".MyService"
    android:exported="true"
    android:process=":MyCorn" >
    <intent-filter>
        <action android:name="com.example.androidtest.myservice" />
    </intent-filter>
</service>

4)Started Service ClientService通訊相關

Client呼叫startService(Intent serviceIntent)啟動Service時,Client可以將引數通過Intent直接傳遞給ServiceService執行過程中,如果需要將引數傳遞給Client,一般可以通過藉助於傳送廣播的方式(此時,Client需要註冊此廣播)。

3.Bound Service

相對於Started ServiceBound Service具有更多的知識點。Bound Service的主要特性在於Service的生命週期是依附於Client的生命週期的,當Client不存在時,Bound Service將執行onDestroy,同時通過Service中的Binder物件可以較為方便進行Client-Service通訊。Bound Service一般使用過程如下:

1.自定義Service繼承基類Service,並重寫onBind(Intent intent)方法,此方法中需要返回具體的Binder物件;

2.Client通過實現ServiceConnection介面來自定義ServiceConnection,並通過bindService (Intent service, ServiceConnection sc, int flags)方法將Service繫結到此Client上;

3.自定義的ServiceConnection中實現onServiceConnected(ComponentName name, IBinder binder)方法,獲取Service端Binder例項;

4.通過獲取的Binder例項進行Service端其他公共方法的呼叫,以完成Client-Service通訊;

5.當Client在恰當的生命週期(如onDestroy等)時,此時需要解綁之前已經繫結的Service,通過呼叫函式unbindService(ServiceConnection sc)。

在Bound Service具體使用過程中,根據onBind(Intent intent)方法放回的Binder物件的定義方式不同,又可以將其分為以下三種方式,且每種方式具有不同的特點和適用場景:

1).Extending the Binder class

這是Bound Service中最常見的一種使用方式,也是Bound Service中最簡單的一種。

侷限:Clinet與Service必須同屬於同一個程式,不能實現程式間通訊(IPC)。否則則會出現類似於“android.os.BinderProxy cannot be cast to xxx”錯誤。

下面通過程式碼片段看下具體的使用:

public class MyBindService extends Service {

    public static final String TAG = "MyBindService";

    private MyBinder mBinder = new MyBinder();

    public class MyBinder extends Binder {
        MyBindService getService() {
            return MyBindService.this;
        }
    }

    @Override
    public void onCreate() {
        super.onCreate();
        Log.w(TAG, "in onCreate");
    }

    @Override
    public IBinder onBind(Intent intent) {
        Log.w(TAG, "in onBind");
        return mBinder;
    }

    @Override
    public boolean onUnbind(Intent intent) {
        Log.w(TAG, "in onUnbind");
        return super.onUnbind(intent);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.w(TAG, "in onDestroy");
    }
}
public class BActivity extends Activity {

    public static final String TAG = "BActivity";

    private Button bindServiceBtn;
    private Button unbindServiceBtn;

    private Button startIntentService;

    private Intent serviceIntent;

    private ServiceConnection sc = new MyServiceConnection();
    private MyBinder mBinder;
    private MyBindService mBindService;
    private boolean mBound;

    private class MyServiceConnection implements ServiceConnection {

        @Override
        public void onServiceConnected(ComponentName name, IBinder binder) {
            Log.w(TAG, "in MyServiceConnection onServiceConnected");
            mBinder = (MyBinder) binder;
            mBindService = mBinder.getService();

            mBound = true;
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            // This is called when the connection with the service has been
            // unexpectedly disconnected -- that is, its process crashed.
            Log.w(TAG, "in MyServiceConnection onServiceDisconnected");
            mBound = false;
        }

    }

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

        bindServiceBtn = (Button) findViewById(R.id.bind_service);
        unbindServiceBtn = (Button) findViewById(R.id.unbind_service);
        startIntentService = (Button) findViewById(R.id.start_intentservice);

        bindServiceBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(BActivity.this, MyBindService.class);
                bindService(intent, sc, Context.BIND_AUTO_CREATE);
            }
        });

        unbindServiceBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                excuteUnbindService();
            }
        });

        startIntentService.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(BActivity.this, MyIntentService.class);
                startService(intent);
            }
        });

    }

    private void excuteUnbindService() {
        if (mBound) {
            unbindService(sc);
            mBound = false;
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        Log.w(TAG, "in onDestroy");
        excuteUnbindService();
    }
}

首次點選bindServiceBtn進行bindService(..)時,依次回撥順序如下:

MyBindService(13457): in onCreate
MyBindService(13457): in onBind
BActivity(13457): in MyServiceConnection onServiceConnected

再次點選bindServiceBtn按鈕時,發現沒有任何輸出,說明MyBindService沒有進行任何回撥。

點選unbindServiceBtn進行unbindService(..)時,回撥順序為:

MyBindService(13457): in onUnbind
MyBindService(13457): in onDestroy

注:在四大基本元件中,需要注意的的是BroadcastReceiver不能作為Bound ServiceClient,因為BroadcastReceiver的生命週期很短,當執行完onReceive(..)回撥時,BroadcastReceiver生命週期完結。而Bound Service又與Client本身的生命週期相關,因此,Android中不允許BroadcastReceiverbindService(..),當有此類需求時,可以考慮通過startService(..)替代。

2)Using a Messenger

Messenger,在此可以理解成”信使“,通過Messenger方式返回Binder物件可以不用考慮Clinet - Service是否屬於同一個程式的問題,並且,可以實現Client - Service之間的雙向通訊。極大方便了此類業務需求的實現。

侷限:不支援嚴格意義上的多執行緒併發處理,實際上是以佇列去處理

下面直接看下具體的使用:

public class MyMessengerService extends Service {

    public static final String TAG = "MyMessengerService";

    public static final int MSG_FROM_CLIENT_TO_SERVER = 1;
    public static final int MSG_FROM_SERVER_TO_CLIENT = 2;

    private Messenger mClientMessenger;
    private Messenger mServerMessenger = new Messenger(new ServerHandler());

    @Override
    public IBinder onBind(Intent intent) {
        Log.w(TAG, "in onBind");
        return mServerMessenger.getBinder();
    }

    class ServerHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            Log.w(TAG, "thread name:" + Thread.currentThread().getName());
            switch (msg.what) {
            case MSG_FROM_CLIENT_TO_SERVER:
                Log.w(TAG, "receive msg from client");
                mClientMessenger = msg.replyTo;

                // service傳送訊息給client
                Message toClientMsg = Message.obtain(null, MSG_FROM_SERVER_TO_CLIENT);
                try {
                    Log.w(TAG, "server begin send msg to client");
                    mClientMessenger.send(toClientMsg);
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
                break;
            default:
                super.handleMessage(msg);
            }
        }
    }

    @Override
    public boolean onUnbind(Intent intent) {
        Log.w(TAG, "in onUnbind");
        return super.onUnbind(intent);
    }

    @Override
    public void onDestroy() {
        Log.w(TAG, "in onDestroy");
        super.onDestroy();
    }
}
public class CActivity extends Activity {

    public static final String TAG = "CActivity";

    private Button bindServiceBtn;
    private Button unbindServiceBtn;
    private Button sendMsgToServerBtn;

    private ServiceConnection sc = new MyServiceConnection();
    private boolean mBound;

    private Messenger mServerMessenger;

    private Handler mClientHandler = new MyClientHandler();
    private Messenger mClientMessenger = new Messenger(mClientHandler);

    private class MyClientHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            if (msg.what == MyMessengerService.MSG_FROM_SERVER_TO_CLIENT) {
                Log.w(TAG, "reveive msg from server");
            }
        }
    }

    private class MyServiceConnection implements ServiceConnection {

        @Override
        public void onServiceConnected(ComponentName name, IBinder binder) {
            Log.w(TAG, "in MyServiceConnection onServiceConnected");
            mServerMessenger = new Messenger(binder);

            mBound = true;
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            // This is called when the connection with the service has been
            // unexpectedly disconnected -- that is, its process crashed.
            Log.w(TAG, "in MyServiceConnection onServiceDisconnected");

            mBound = false;
        }
    }

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

        bindServiceBtn = (Button) findViewById(R.id.bind_service);
        unbindServiceBtn = (Button) findViewById(R.id.unbind_service);
        sendMsgToServerBtn = (Button) findViewById(R.id.send_msg_to_server);

        bindServiceBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(CActivity.this, MyMessengerService.class);
                bindService(intent, sc, Context.BIND_AUTO_CREATE);
            }
        });

        unbindServiceBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                excuteUnbindService();
            }
        });

        sendMsgToServerBtn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                sayHello();
            }
        });

        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                Intent intent = new Intent(CActivity.this, MyAlarmBroadcastReceiver.class);
                sendBroadcast(intent);
            }
        }, 3 * 1000);

    }

    public void sayHello() {
        if (!mBound)
            return;
        // Create and send a message to the service, using a supported 'what' value
        Message msg = Message.obtain(null, MyMessengerService.MSG_FROM_CLIENT_TO_SERVER, 0, 0);
        // 通過replyTo把client端的Messenger(信使)傳遞給service
        msg.replyTo = mClientMessenger;
        try {
            mServerMessenger.send(msg);
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

    private void excuteUnbindService() {
        if (mBound) {
            unbindService(sc);
            mBound = false;
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        Log.w(TAG, "in onDestroy");
        excuteUnbindService();
    }
}

其中,需要注意的幾點是:

1.MyMessengerService自定中,通過new Messenger(new ServerHandler())建立Messenger物件,在onBind(..)回撥中,通過呼叫Messenger物件的getBinder()方法,將Binder返回;

2.Client在ServiceConnectiononServiceConnected(..)的回撥中,通過new Messenger(binder)獲取到Service傳遞過來的mServerMessenger

3.接下來,就可以通過mServerMessenger.send(msg)方法向Service傳送messageService中的Messenger構造器中的Handler即可接收到此資訊,在handleMessage(..)回撥中處理;

4.至此只是完成了從Client傳送訊息到Service,同樣的道理,想實現Service傳送訊息到Client,可以在客戶端定義一個Handler,並得到相應的Messenger,在Clinet傳送訊息給Service時,通過msg.replyTo = mClientMessenger方式將Client信使傳遞給Service;

5.Service接收到Client信使後,獲取此信使,並通過mClientMessenger.send(toClientMsg)方式將Service訊息傳送給Client。

至此,完成了Client - Service之間的雙向通訊流程。

3).AIDL(Android Interface Definition Language)

一般情況下,Messenger這種方式都是可以滿足需求的,當然,通過自定義AIDL方式相對更加靈活。

這種方式需要自己在專案中自定義xxx.aidl檔案,然後系統會自動在gen目錄下生成相應的介面類檔案,接下來整個的流程與Messenger方式差別不大,網上也有不少例項,在此不再具體給出。

注:無論哪種方式的Bound Service,在進行unbind(…)操作時,都需要注意當前Service是否處於已經繫結狀態,否則可能會因為當前Service已經解綁後繼續執行unbind(…)會導致崩潰。這點與Started Service區別很大(如前文所述:stopService(…)無需做當前Service是否有效的判斷)。

4.Local Service VS Remote Service

Local Service:不少人又稱之為”本地服務“,是指Client - Service同處於一個程式;

Remote Service:又稱之為”遠端服務“,一般是指Service處於單獨的一個程式中。

其他使用上上文中基本上都有所述。

5.Service特性

1.Service本身都是執行在其所在程式的主執行緒(如果Service與Clinet同屬於一個程式,則是執行於UI執行緒),但Service一般都是需要進行”長期“操作,所以經常寫法是在自定義Service中處理”長期“操作時需要新建執行緒,以免阻塞UI執行緒或導致ANR;

2.Service一旦建立,需要停止時都需要顯示呼叫相應的方法(Started Service需要呼叫stopService(…)或Service本身呼叫stopSelf(…), Bound Service需要呼叫unbindService(…)),否則對於Started Service將處於一直執行狀態,對於Bound Service,當Client生命週期結束時也將因此問題。也就是說,Service執行完畢後,必須人為的去停止它。

6.IntentService

IntentService是系統提供給我們的一個已經繼承自Service類的特殊類,IntentService特殊性是相對於Service本身的特性而言的:

1.預設直接實現了onBind(..)方法,直接返回null,並定義了抽象方法onHandlerIntent(..),使用者自定義子類時,需要實現此方法;

2.onHandlerIntent(..)主要就是用來處於相應的”長期“任務的,並且已經自動在新的執行緒中,使用者無語自定義新執行緒;

3.當”長期“任務執行完畢後(也就是onHandlerIntent(..)執行完畢後),此IntentService將自動結束,無需人為呼叫方法使其結束;

4.IntentService處於任務時,也是按照佇列的方式一個個去處理,而非真正意義上的多執行緒併發方式。

下面是一個基本的繼承自IntentService的自定義Service

public class MyIntentService extends IntentService {

    public static final String TAG = "MyIntentService";

    public MyIntentService() {
        super(TAG);
    }

    public MyIntentService(String name) {
        super(name);
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        Log.w(TAG, "in onHandleIntent");
        Log.w(TAG, "thread name:" + Thread.currentThread().getName());
    }

}

7.前臺Service

Android中Service介面中還提供了一個稱之為”前臺Service“的概念。通過Service.startForeground (int id, Notification notification)方法可以將此Service設定為前臺Service。在UI顯示上,notification將是一個處於onGoing狀態的通知,使得前臺Service擁有更高的程式優先順序,並且Service可以直接notification通訊。

下面是一個簡單的前臺Service使用例項:

public class MyService extends Service {

    public static final String TAG = "MyService";

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        Log.w(TAG, "in onCreate");
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.w(TAG, "in onStartCommand");
        Log.w(TAG, "MyService:" + this);
        String name = intent.getStringExtra("name");
        Log.w(TAG, "name:" + name);

        
        Notification notification = new Notification(R.drawable.ic_launcher, "test", System.currentTimeMillis());
        Intent notificationIntent = new Intent(this, DActivity.class);
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntesnt, 0);
        notification.setLatestEventInfo(this, "title", "content", pendingIntent);
        startForeground(1, notification);
        

        return START_REDELIVER_INTENT;
    }

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

相關文章