從一個例子開始分析AIDL原理

上一個項目(下載中心)使用到了AIDL相關的技術,趁現在項目不是特別忙,總結一下。

首先第一個問題,AIDL是個什么?它的全稱叫 Android Interface Definition Language,中文叫做安卓接口定義語言,這里面有兩個關鍵字,“Interface”和“Language",從這兩個關鍵字來看它是一門用于定義接口的語言,既然是語言那自然就有它的語法與規則,但是本著先實現一個例子再回過頭來學習語法的原則,下一篇文章再詳細說明AIDL的語法。坦率講,即使你不了解AIDL語法,基本上也能看懂,因為它與Java非常相似。下面通過一個例子來展示如何通過AIDL來實現跨進程通信(IPC)。

假設這樣一個場景:有一個DownloadCenter,它可以向外提供下載服務,其他App有下載需求的話就可以通過它提供的服務來完成下載,這種方式非常類似于C/S模型。很明顯DownloadCenter和其他App不是在同一個進程中,不能直接調用,就需要通過IPC機制來完成,而Android下的IPC方式有很多,比如:通過Inten傳遞數據、文件共享、Messenger、ContentProvider、aidl、Socket等,根據業務需求,選用aidl。

一.先寫一個例子

我們通常把提供服務的一方叫做服務端,請求服務的一方叫做客戶端,這里就把服務端命名為 DownloadCenter,客戶端就叫 Client,接下來就開始實現這個例子。

(1) 創建一個普通的Android工程DownloadCenter,接著創建一個包 com.jdqm.downloadcenter.aidl,在這個包下創建一個DownloadTask類。

public class DownloadTask implements Parcelable{

    private int id;

    private String url;

    public DownloadTask() {
    }

    public DownloadTask(int id, String url) {
        this.id = id;
        this.url = url;
    }

    protected DownloadTask(Parcel in) {
        id = in.readInt();
        url = in.readString();
    }

    public static final Creator<DownloadTask> CREATOR = new Creator<DownloadTask>() {
        @Override
        public DownloadTask createFromParcel(Parcel in) {
            return new DownloadTask(in);
        }

        @Override
        public DownloadTask[] newArray(int size) {
            return new DownloadTask[size];
        }
    };

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(id);
        dest.writeString(url);
    }

    public void readFromParcel(Parcel in) {
        id = in.readInt();
        url = in.readString();
    }

    //重寫toString方法方便打印
    @Override
    public String toString() {
        return "DownloadTask{" +
                "id=" + id +
                ", url='" + url + '\'' +
                '}';
    }
}

這個類代碼看起來很多,其實就定義了兩個成員變量id和url,并且實現Parcelable接口,其他的代碼都是AS生成的。為什么要實現Parcelable接口?這是因為在跨進程通信過程中,涉及到對象數據的序列化和反序列化。關于Android中實現對象的序列化通常可以實現Parcelable或者Serializable接口,為了更好的理解建議還是先熟悉對象的序列化和反序列化相關的知識,這里就不再深入了。

插播一個場景:當初在學對象的序列化和反序列化的時候萌生了這樣一個幻想,如今春節車票真的是一票難求,想回家的你費盡了心思也沒買到一張二等座。假設有這么一個設備能把一個人(對象)序列化,然后通過網絡傳輸到家里,再反序列化出來,這該是多么美好的一件事!

(2) 在(1)中的包名右鍵,New->AIDL->AIDL File 創建一個aild文件 DownloadTask.aidl

// DownloadTask.aidl
package com.jdqm.downloadcenter.aidl;

parcelable DownloadTask;

當你在(1)中的包名右鍵新建一個AIDL文件時,項目的目錄結構中main下面多了一個aidl的文件夾,并且會幫你創建一個與你右鍵的地方相同的包名。緊接著在相同的包名的創建IDownloadCenter.aidl

// IDownloadCenter.aidl
package com.jdqm.downloadcenter.aidl;

import com.jdqm.downloadcenter.aidl.DownloadTask;

interface IDownloadCenter {
    //添加下載任務
    void addDownloadTask(in DowloadTask task);
    
    //查詢所有的添加的下載任務
    List<DownloadTask> getDownloadTask();
}

到這里,你需要Build一下項目,一來是檢查有沒有錯誤,更重要的是讓Android SDK Tool根據aidl文件生成對應的Java文件,如果Build成功,那么會在app/build/generated/source/aidl/debug下生成IDownloadCenter.java這個文件(切換到Project視圖,或者直接double shift搜索更快)。

(3) 創建服務 DownloadCenterService

public class DownloadCenterService extends Service {

    private List<DownloadTask> tasks;

    private DownloadCenter downloadCenter;

    @Override
    public void onCreate() {
        super.onCreate();
        
        //由于是在Binder線程池中訪問這個集合,所以有必要好線程同步。除非你能確保并發情況下不會出現問題
        tasks = Collections.synchronizedList(new ArrayList<DownloadTask>());
        downloadCenter = new DownloadCenter();
    }

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

    /**
     * 完成aidl文件編寫后,下次Build時Android SDK tools會根據IDownloadCenter.aidl文件生成
     * IDownloadCenter.java文件,Stub是它的內部類
     */
    private class DownloadCenter extends IDownloadCenter.Stub {

        @Override
        public void addDownloadTask(DownloadTask task) throws RemoteException {
            tasks.add(task);
        }

        @Override
        public List<DownloadTask> getDownloadTask() throws RemoteException {
            return tasks;
        }
    }
}

這個Service其實也很簡單,首先創建一個IDownloadCenter.Stub的實現類DownloadCenter,并且實現其內部的兩個抽象方法(可以看到這兩個方法就是在aidl文件中聲明的方法),然后在onBind()方法將其返回它的一個實例。

(4) 最后別忘了在AndroidManifest.xml文件中注冊這個Service,另外為了讓其他應用通過隱式Intent啟動,需要給這個Service添加一個intent-filter

<service android:name=".DownloadCenterService">
    <intent-filter>
        <action android:name="jdqm.intent.action.LAUNCH"/>
        <category android:name="android.intent.category.DEFAULT"/>
    </intent-filter>
</service>

接下來是客戶端Client的實現:

(1) 首先創建一個名為Client的Android工程,然后將服務端的所有aidl文件以及aidl文件中使用到的Java類拷貝到Cilent中,注意包名結構也要和服務端一致,如果不一致在反序列化的時候就會出錯。

(2) 通過bindService與服務端建立連接,完成服務方法調用

public class MainActivity extends AppCompatActivity implements View.OnClickListener {

    private static final String TAG = "MainActivity";

    private Button btnAddTask;
    private Button btnGetTasks;

    private DownloadServiceConn serviceConn;
    private IDownloadCenter downloadCenter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initViews();
        serviceConn = new DownloadServiceConn();
        Intent intent = new Intent("jdqm.intent.action.LAUNCH");
        intent.setPackage("com.jdqm.downloadcenter");
        bindService(intent, serviceConn, BIND_AUTO_CREATE);
    }

    private void initViews() {
        btnAddTask = findViewById(R.id.btnAddTask);
        btnGetTasks = findViewById(R.id.btnGetTasks);
        btnAddTask.setOnClickListener(this);
        btnGetTasks.setOnClickListener(this);
        btnAddTask.setEnabled(false);
        btnGetTasks.setEnabled(false);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.btnAddTask:
                try {
                    Random random = new Random();
                    int id = random.nextInt(10);
                    DownloadTask task = new DownloadTask(id, "http://test.jdqm.com/test.aidl");
                    
                    //在客戶端直接調用IBinder接口的方法,最終服務端對應的方法會調用,這似乎是在同一個進程中一般
                    //這是因為Binder機制已經把底層的實現隱藏掉了
                    downloadCenter.addDownloadTask(task);
                    Log.d(TAG, "添加任務成功: " + task);
                } catch (RemoteException e) {
                    e.printStackTrace();
                    Log.e(TAG, "添加任務失敗");
                }
                break;
            case R.id.btnGetTasks:
                try {
                    List<DownloadTask> tasks = downloadCenter.getDownloadTask();
                    Log.d(TAG, "從服務中獲取到已經添加的任務列表: " + tasks);
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
                break;
            default:
                break;
        }
    }

    private class DownloadServiceConn implements ServiceConnection {

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            Log.d(TAG, "服務已連接");

            //將綁定服務的返回的IBinder對象轉換為目標接口類型
            downloadCenter = IDownloadCenter.Stub.asInterface(service);
            btnAddTask.setEnabled(true);
            btnGetTasks.setEnabled(true);
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            Log.d(TAG, "服務已斷開");
            btnAddTask.setEnabled(false);
            btnGetTasks.setEnabled(false);
        }
    }

    @Override
    protected void onDestroy() {
        unbindService(serviceConn);
        super.onDestroy();
    }
}

以上是完整的客戶端實現,首先bindService,通過ServiceConnection完成綁定時的回調方法onServiceConnected拿到服務端返回的IBinder對象,通過IDownloadCenter.Stub.asInterface()方法將IBinder轉換為目標IBinder類型IDownloadCenter,通過它來完成IDownloadCenter.Stub實現類方法的調用。

現在將DownloadCenter和Client安裝到同一個Android設備中,打開客戶端,點擊界面里的按鈕,下面是輸出的log信息:

/com.jdqm.client D/MainActivity: 服務已連接
/com.jdqm.client D/MainActivity: 添加任務成功: DownloadTask{id=6, url='http://test.jdqm.com/test.aidl'}
/com.jdqm.client D/MainActivity: 從服務中獲取到已經添加的任務列表: [DownloadTask{id=6, url='http://test.jdqm.com/test.aidl'}]
/com.jdqm.client D/MainActivity: 添加任務成功: DownloadTask{id=5, url='http://test.jdqm.com/test.aidl'}
/com.jdqm.client D/MainActivity: 從服務中獲取到已經添加的任務列表: [DownloadTask{id=6, url='http://test.jdqm.com/test.aidl'}, DownloadTask{id=5, url='http://test.jdqm.com/test.aidl'}]

到這里,我們完成了客戶端與服務端的跨進程通信。雖然沒有像唐僧西天取經那樣歷經九九81難(何苦呢),但也可以回頭看看我們走過路了。

二.回頭看看

先來總結一下步驟:

  1. 在src/main/aidl/下創建了兩個 aidl文件:DownloadTask.aidl、IDownloadCenter.aidl;
  2. 創建一個IDownloadCenter.Stub實現類,并在onBind中返回一個實例: DownloadCenter extends IDownloadCenter.Stub;
  3. 在客戶端實現一個ServiceConnection;
  4. Context.bindService(), 在回調onServiceConnected()中接收IBinder實例, 并且調用IDownloadCenter.Stub.asInterface(service)將返回值轉換為IDownloadCenter類型。
  5. 調用IDownloadCenter接口中定義的方法來完成跨進程調用;
  6. 調用Context.unbindService()斷開連接。
兩個項目結構

粗略地總結好像并沒什么意思,畢竟大多數真諦是隱藏在細節中的。就好比讓你回憶初戀的過程,可能你更想回憶起接吻時的感覺! 首先從IDownloadCenter.aidl生成的IDownloadCenter.java開始。

public interface IDownloadCenter extends android.os.IInterface {

    public static abstract class Stub extends android.os.Binder implements com.jdqm.downloadcenter.aidl.IDownloadCenter {
       //省略器內部實現
    }
    
    //添加下載任務
    public void addDownloadTask(com.jdqm.downloadcenter.aidl.DownloadTask task) throws android.os.RemoteException;
    
    //查詢所有的添加的下載任務
    public java.util.List<com.jdqm.downloadcenter.aidl.DownloadTask> getDownloadTask() throws android.os.RemoteException;
}

打開這個文件代碼看起來是真的有點多(130多行),而且格式不太好閱讀,所以最好是先格式化一下。所幸的是它的結構很清晰,首先它是一個接口,內部定義了兩個接口方法和一個內部類Stub,顯然這兩個方法就是在aidl文件中定義的方法,連注釋都給你搬過來了,可見這個生成工具是如此地用心。重點就是這個內部類Stub,它是一個抽象類,并且實現了IDownloadCenter接口,但它并沒有實現接口中的兩個方法。還記得在Service中IDownloadCenter.Stub的實現類嗎?

private class DownloadCenter extends IDownloadCenter.Stub {

    @Override
    public void addDownloadTask(DownloadTask task) throws RemoteException {
        tasks.add(task);
    }

    @Override
    public List<DownloadTask> getDownloadTask() throws RemoteException {
        return tasks;
    }
}

接著我們在Service的onBind方法返回了它的一個實例,創建這個Binder的過程做了什么?那就得窺探窺探Stub的真容了。

public static abstract class Stub extends android.os.Binder implements com.jdqm.downloadcenter.aidl.IDownloadCenter {
        private static final java.lang.String DESCRIPTOR = "com.jdqm.downloadcenter.aidl.IDownloadCenter";

        public Stub() {
            this.attachInterface(this, DESCRIPTOR);
        }

        public static com.jdqm.downloadcenter.aidl.IDownloadCenter asInterface(android.os.IBinder obj) {
            if ((obj == null)) {
                return null;
            }
            android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
            if (((iin != null) && (iin instanceof com.jdqm.downloadcenter.aidl.IDownloadCenter))) {
                return ((com.jdqm.downloadcenter.aidl.IDownloadCenter) iin);
            }
            return new com.jdqm.downloadcenter.aidl.IDownloadCenter.Stub.Proxy(obj);
        }

        @Override
        public android.os.IBinder asBinder() {
            return this;
        }

        @Override
        public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {
            //省略內部實現
        }

        private static class Proxy implements com.jdqm.downloadcenter.aidl.IDownloadCenter {
            //省略內部實現
        }

        static final int TRANSACTION_addDownloadTask = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
        static final int TRANSACTION_getDownloadTask = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
    }

可以看到,在Stub的構造方法中調了this.attachInterface(this, DESCRIPTOR)方法,這個DESCRIPTOR就是IDownloadCenter的完整名稱。

Binder#attachInterface

public void attachInterface(IInterface owner, String descriptor) {
    mOwner = owner;
    mDescriptor = descriptor;
}

可以看到把當前對象保存到Binder的mOwner,將接口的完整名稱保存到Binder的mDescriptor。至此onBind返回了這個對象,交由底層的Binder機制來處理,底層原理之復雜度猶如乾坤大挪移一般深噢,將邏輯轉到了客戶端。

@Override
public void onServiceConnected(ComponentName name, IBinder service) {
    Log.d(TAG, "服務已連接" );
    Log.d(TAG, "ComponentName: " + name.getClassName()); //com.jdqm.downloadcenter.DownloadCenterService
    Log.d(TAG, "service: " + service.getClass().getName()); //android.os.BinderProxy
    //將綁定服務的返回的IBinder對象轉換為目標接口類型
    downloadCenter = IDownloadCenter.Stub.asInterface(service);
    btnAddTask.setEnabled(true);
    btnGetTasks.setEnabled(true);
}

這個方法是在客戶端與服務端建立連接完成時回調,這個service到底是什么,是Service中onBind返回的DownloadCenter嗎?通過 service.getClass().getName()發現它是一個BinderProxy,接著調用IDownloadCenter.Stub.asInterface(service),它的實現如下:

public static com.jdqm.downloadcenter.aidl.IDownloadCenter asInterface(android.os.IBinder obj) {
    if ((obj == null)) {
        return null;
    }
    
    //這個obj是BinderProxy
    android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR); //返回null
    if (((iin != null) && (iin instanceof com.jdqm.downloadcenter.aidl.IDownloadCenter))) {
        return ((com.jdqm.downloadcenter.aidl.IDownloadCenter) iin);
    }
    return new com.jdqm.downloadcenter.aidl.IDownloadCenter.Stub.Proxy(obj);
}

首先通過調用了BinderProxy.queryLocalInterface


final class BinderProxy implements IBinder {

    public IInterface queryLocalInterface(String descriptor) {
        return null;
    }
}

它的返回值是null,即 iin 為null,所以 IDownloadCenter.Stub.asInterface(service)得到的是IDownloadCenter.Stub.Proxy的實例。

通過asInterface方法可以得出一個結論:

  • 如果服務端與客戶端在同一個進程,那么在onServiceConnected方法得到的service就是我們在onBind中返回的類型IDownloadCenter.Stub;
  • 如果不是同一個進程,那得到的就是IDownloadCenter.Stub.Proxy類型;

拿到了IDownloadCenter.Stub.Proxy,那就可以調用它的方法了 downloadCenter.addDownloadTask(task),下面是IDownloadCenter.Stub.Proxy的實現:

public static abstract class Stub extends android.os.Binder implements com.jdqm.downloadcenter.aidl.IDownloadCenter {
        
        //省略前面代碼
        @Override
        public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {
            switch (code) {
                case INTERFACE_TRANSACTION: {
                    reply.writeString(DESCRIPTOR);
                    return true;
                }
                case TRANSACTION_addDownloadTask: {
                    data.enforceInterface(DESCRIPTOR);
                    com.jdqm.downloadcenter.aidl.DownloadTask _arg0;
                    if ((0 != data.readInt())) {
                        _arg0 = com.jdqm.downloadcenter.aidl.DownloadTask.CREATOR.createFromParcel(data);
                    } else {
                        _arg0 = null;
                    }
                    this.addDownloadTask(_arg0);
                    reply.writeNoException();
                    return true;
                }
                case TRANSACTION_getDownloadTask: {
                    data.enforceInterface(DESCRIPTOR);
                    java.util.List<com.jdqm.downloadcenter.aidl.DownloadTask> _result = this.getDownloadTask();
                    reply.writeNoException();
                    reply.writeTypedList(_result);
                    return true;
                }
            }
            return super.onTransact(code, data, reply, flags);
        }

        private static class Proxy implements com.jdqm.downloadcenter.aidl.IDownloadCenter {
            private android.os.IBinder mRemote;

            Proxy(android.os.IBinder remote) {
                mRemote = remote;
            }

            @Override
            public android.os.IBinder asBinder() {
                return mRemote;
            }

            public java.lang.String getInterfaceDescriptor() {
                return DESCRIPTOR;
            }
            
            //添加下載任務
            @Override
            public void addDownloadTask(com.jdqm.downloadcenter.aidl.DownloadTask task) throws android.os.RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    if ((task != null)) {
                    
                        //注意這里寫入的是1
                        _data.writeInt(1);
                        
                        //序列化我們傳入的實參,到這里你知道為什么跨進程通信的對象需要實現Parcelable接口了吧
                        task.writeToParcel(_data, 0);
                    } else {
                        _data.writeInt(0);
                    }
                    
                    //這個mRemote就是是BinderProxy
                    mRemote.transact(Stub.TRANSACTION_addDownloadTask, _data, _reply, 0);
                    _reply.readException();
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
            }
            
            //查詢所有的添加的下載任務
            @Override
            public java.util.List<com.jdqm.downloadcenter.aidl.DownloadTask> getDownloadTask() throws android.os.RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                java.util.List<com.jdqm.downloadcenter.aidl.DownloadTask> _result;
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    mRemote.transact(Stub.TRANSACTION_getDownloadTask, _data, _reply, 0);
                    _reply.readException();
                    _result = _reply.createTypedArrayList(com.jdqm.downloadcenter.aidl.DownloadTask.CREATOR);
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
                return _result;
            }
        }

        static final int TRANSACTION_addDownloadTask = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
        static final int TRANSACTION_getDownloadTask = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
    }

從Proxy#addDownloadTask方法可以看到,通過 task.writeToParcel(_data, 0),將我們傳入的DownloadTask對象進行了序列化,終于知道為啥前面寫的DownloadTask要實現Parcelable接口了。接著調用 mRemote.transact(Stub.TRANSACTION_addDownloadTask, _data, _reply, 0)方法,通過前面創建Proxy對象的過程,我們知道這個mRemote是BinderProxy,這樣邏輯就交給了底層的Binder機制,服務端的onTransact就會被調用。接下來就看看onTransact方法中 TRANSACTION_addDownloadTask 這個case的實現:

public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {

    switch (code) {
        case TRANSACTION_addDownloadTask: {
            data.enforceInterface(DESCRIPTOR);
            com.jdqm.downloadcenter.aidl.DownloadTask _arg0;
            if ((0 != data.readInt())) {
                _arg0 = com.jdqm.downloadcenter.aidl.DownloadTask.CREATOR.createFromParcel(data);
            } else {
                _arg0 = null;
            }
            this.addDownloadTask(_arg0);
            reply.writeNoException();
            return true;
        }
    }
    //省略其他內容
}            

首先 if ((0 != data.readInt())) 這個條件是true,因為我們在Proxy#addDownloadTask中寫入的是1。但是這里我有一個疑問,為啥這個if語句多了一層圓括號?你知道嗎?然后通過DownloadTask.CREATOR.createFromParcel(data)將數據反序列化并賦值給 _arg0,接著調用 this.addDownloadTask(_arg0),這個this不就是我們在onBind返回的Stub實例嗎!就這樣服務端方法被調用了。

另外一個方法getDownloadTask()方法的調用過程也是類似,有區別的是它有返回值,所以onTransact()方法時多了 reply.writeTypedList(_result),回到transact()方法時將結果反序列化 _result = _reply.createTypedArrayList(com.jdqm.downloadcenter.aidl.DownloadTask.CREATOR)。下面給出一張粗略的工作流程圖輔助理解這個過程:

Binder

三.最后

  • 在這個例子中,我們是在主線程中調用服務方法downloadCenter.addDownloadTask(task),在執行mRemote.transact(Stub.TRANSACTION_getDownloadTask, _data, _reply, 0)時調用線程會被掛起,所以如果不能確保遠程服務方法的是否耗時時,應該避免在主線程中調用。當然了,aidl還提供了一個關鍵字 oneway,定義方法時加上這個關鍵字就不用等待服務端方法返回了,如果這滿足你的業務不需要等待結果,那也是一個不錯的選擇。
//添加下載任務
oneway void addDownloadTask(in DownloadTask task);
  • 另外一點就是服務端方法是運行在Binder線程池中的,要考慮好線程同步。

最后并不代表結束了,可能又是一個新的開始。細心的讀者可能會留意到前面我們寫的aidl文件:

// IDownloadCenter.aidl
package com.jdqm.downloadcenter.aidl;

import com.jdqm.downloadcenter.aidl.DownloadTask;

interface IDownloadCenter {
    //添加下載任務
    void addDownloadTask(in DownloadTask task);

    //查詢所有的添加的下載任務
    List<DownloadTask> getDownloadTask();
}

其中 addDownloadTask 這個方法的參數前面有一個in,這個 in 是什么?這是一個定向tag,事實上定向tag有3個in、out、inout,它們又是什么含義?下一篇文章將從零開始探索這3個定向tag的含義以及用法。

關注微信公眾號,第一時間接收推送!

客戶端源碼
服務端源碼

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容