第二章 IPC機(jī)制詳解(3)

本文為Android開發(fā)藝術(shù)探索的筆記榕酒,僅供學(xué)習(xí)

4.4 AIDL的使用

前面Messenger進(jìn)程通信中,如果客戶端有大量的消息需要發(fā)送到服務(wù)端,那么服務(wù)端也只能一個(gè)個(gè)處理烈涮,所以在處理大數(shù)據(jù)的時(shí)候使用Messenger并不是好方法。我們可以使用AIDL來實(shí)現(xiàn)跨進(jìn)程窖剑,所以Messenger的底層是AIDL換句話說Messenger就是AIDL坚洽,只不過系統(tǒng)做了封裝方便我們使用。有了Binder的基礎(chǔ)我們可以更好的理解AIDL西土。
服務(wù)端要?jiǎng)?chuàng)建一個(gè)Service去監(jiān)聽客戶端發(fā)來的消息讶舰,然后建立一個(gè)AIDL的文件夾,將暴露給客戶端的接口在AIDL文件夾里聲明需了,最后在Service去實(shí)現(xiàn)這個(gè)AIDL即可跳昼。
客戶端,需要綁帶Service肋乍,綁帶成功后把服務(wù)端返回的Binder轉(zhuǎn)化為AIDL所屬的類型鹅颊,接著調(diào)用AIDL里的方法即可。
我們來舉個(gè)例子住拭,大致的業(yè)務(wù)邏輯就是主要有三個(gè)功能挪略,1.客戶端可以向服務(wù)端添加Book 2.客戶端可以向服務(wù)端獲取Book信息 3.向服務(wù)端添加Book監(jiān)聽,監(jiān)聽每次添加新Book的信息

下面上代碼
AIDL
// Book.aidl
package com.example.gyh.myapplication;
// Declare any non-default types here with import statements
parcelable Book;
該類主要就是用來聲明Book這個(gè)Bean

// IOnNewBookArrivedListener.aidl
package com.example.gyh.myapplication;
// Declare any non-default types here with import statements
import com.example.gyh.myapplication.Book;
interface IOnNewBookArrivedListener {
    /**
     * Demonstrates some basic types that you can use as parameters
     * and return values in AIDL.
     */
  void onNewBookArrived(in Book newBook);
}


// IBookManager.aidl
package com.example.gyh.myapplication;
// Declare any non-default types here with import statements
import com.example.gyh.myapplication.Book;
import com.example.gyh.myapplication.IOnNewBookArrivedListener;
interface IBookManager {
    /**
     * Demonstrates some basic types that you can use as parameters
     * and return values in AIDL.
     */
  void addBook(in Book book);
  List<Book> getBookList();
  void registerListener(IOnNewBookArrivedListener listener);
  void unregisterListener(IOnNewBookArrivedListener listener);

接下來附上服務(wù)端的代碼,正如前面Binder機(jī)制一樣滔岳,創(chuàng)建AIDL業(yè)務(wù)的接口IBookManager.Stub這是運(yùn)行在客戶端的杠娱,然后就會(huì)自動(dòng)生成方法。

public class ServiceBook extends Service {
    private CopyOnWriteArrayList<Book> mBookList = new CopyOnWriteArrayList<Book>();//CopyOnWriteArrayList支持并發(fā)的讀寫
    //    private CopyOnWriteArrayList<IOnNewBookArrivedListener> mlListeners = new CopyOnWriteArrayList<IOnNewBookArrivedListener>();不支持多進(jìn)程對(duì)Listener的增刪
    private RemoteCallbackList<IOnNewBookArrivedListener> mListenerList = new RemoteCallbackList<IOnNewBookArrivedListener>();//為什么要用這方式的List谱煤?因?yàn)镽emoteCallbackList支持多進(jìn)程對(duì)Listener的增刪
    String TAG = "Service";
    boolean isadd = true;

    private Binder binder = new IBookManager.Stub() {//里面的方法和AIDL接口一一對(duì)應(yīng)
        @Override
        public void addBook(Book book) throws RemoteException {
            mBookList.add(book);
        }

        @Override
        public List<Book> getBookList() throws RemoteException {
            return mBookList;
        }

        @Override
        public void registerListener(IOnNewBookArrivedListener listener) throws RemoteException {
//            if (!mlListeners.contains(listener)) {
//                mlListeners.add(listener);
//            } else {
//                Log.i(TAG, "have the same");
//            }
            mListenerList.register(listener);
            Log.i(TAG, "register size" + mListenerList.beginBroadcast());
            mListenerList.finishBroadcast();//每一次執(zhí)行完都要finish一下

        }

        @Override
        public void unregisterListener(IOnNewBookArrivedListener listener) throws RemoteException {
//            if (mlListeners.contains(listener)) {
//                mlListeners.remove(listener);
//            } else {
//                Log.i(TAG, "no find");
//            }
            mListenerList.unregister(listener);
            Log.i(TAG, "unregister size " + mListenerList.beginBroadcast());
            mListenerList.finishBroadcast();

        }
    };

    @Override
    public void onCreate() {
        super.onCreate();
        mBookList.add(new Book(1, "ios"));
        mBookList.add(new Book(2, "android"));
        new Thread(new ServiceWorker()).start();//定義一個(gè)線程摊求,每隔兩秒去增加一個(gè)Book,目的是為了驗(yàn)證監(jiān)聽是否成功
    }

    @Override
    public void onDestroy() {
        isadd = false;

        super.onDestroy();

    }

    void addNewbook(Book book) throws RemoteException {
        mBookList.add(book);
        final int N = mListenerList.beginBroadcast();
        for (int i = 0; i < N; i++) {
            IOnNewBookArrivedListener l = mListenerList.getBroadcastItem(i);
            if (l != null) {
                try {
                    l.onNewBookArrived(book);//給這個(gè)回掉賦值刘离,以并于客戶端在使用時(shí)有返回值
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
            }
        }
        mListenerList.finishBroadcast();
    }

    @Override
    public IBinder onBind(Intent intent) {
        // TODO: Return the communication channel to the service.
        return binder;
    }

    private class ServiceWorker implements Runnable {
        @Override
        public void run() {
            while (isadd) {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                int id = mBookList.size() + 1;
                String name = mBookList.size() + 1 + "";
                try {
                    addNewbook(new Book(id, name));
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

下面是客戶端的代碼
通過服務(wù)端返回的Binder對(duì)于也就是 IBookManager.Stub()對(duì)象室叉,里面的asInterface方法可以返回對(duì)于的AIDL接口,沒印象的可以看看IPC機(jī)制詳解(1),從而去調(diào)用相應(yīng)的方法

public class Main2Activity extends AppCompatActivity {
    String TAG = "Main2";
    IBookManager manager;

    ServiceConnection connection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            IBookManager iBookManager = IBookManager.Stub.asInterface(service);/通過這個(gè)Binder去使用服務(wù)端的方法
            manager = iBookManager;
            List<Book> list = new ArrayList<>();
            try {
                list = iBookManager.getBookList();//服務(wù)端的方法
                Log.i(TAG, list.size() + " " + list.get(0).getName());
                iBookManager.addBook(new Book(3, "nihao"));//服務(wù)端的方法
                Log.i(TAG, list.size() + " " + list.get(list.size() - 1).getName());
                iBookManager.registerListener(listener);//服務(wù)端的方法
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
    };
    IOnNewBookArrivedListener listener = new IOnNewBookArrivedListener.Stub() {
        @Override
        public void onNewBookArrived(Book newBook) throws RemoteException {
            if (newBook != null) {
                Log.i(TAG, "Add new Book" + newBook.getId() + "  " + newBook.getName());//因?yàn)樵诜?wù)端的Addnewbook的方法里添加了回掉的數(shù)據(jù)硫惕,所以newBook是有值得
            }
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main2);
        Intent intent = new Intent(Main2Activity.this, ServiceBook.class);
        bindService(intent, connection, BIND_AUTO_CREATE);
    }

    @Override
    protected void onDestroy() {
        Log.i(TAG, "Destory");
        if (manager != null && manager.asBinder().isBinderAlive()) {
            try {
                Log.i(TAG, "unregister activity" + manager);
                manager.unregisterListener(listener);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }//當(dāng)Activity銷毀的時(shí)候茧痕,去接觸注冊(cè)
        unbindService(connection);
        super.onDestroy();
    }
}

4.5 ContentProvider的使用

ContentProvider在Android中專門用于不同App之間的數(shù)據(jù)共享的,由此可見ContentProvider天生就可以用來實(shí)現(xiàn)跨進(jìn)程通信恼除。ContentProvider的底層也是用到了Binder踪旷,可見Binder在Android系統(tǒng)中是多么的重要曼氛。雖然ContentProvider的底層是Binder,但是系統(tǒng)已經(jīng)為我們封裝好了令野,使用起來也比AIDL要簡(jiǎn)單的多舀患。

那么我們就來自定義個(gè)ContentProvider,首先去建一個(gè)類叫BookProvider 繼承ContentProvider气破,

public class BookProvider extends ContentProvider {
    @Override
    //可以進(jìn)行一些初始化 該方法運(yùn)行在主線程里聊浅,其他五個(gè)方法運(yùn)行在Binder線程池里
    public boolean onCreate() {
        return false;
    }

    @Nullable
    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        return null;
    }

    @Nullable
    @Override
    //用于返回Uri請(qǐng)求對(duì)于的MIME類型(媒體類型)
    public String getType(Uri uri) {
        return null;
    }

    @Nullable
    @Override
    public Uri insert(Uri uri, ContentValues values) {
        return null;
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        return 0;
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        return 0;
    }
}

接著我們要去注冊(cè)ContentProvider


<provider
    android:name=".ContentProvider_text.BookProvider"
    android:authorities="com.example.gyh.myapplication.ContentProvider_text.BookProvider"http://是ContentProvider的唯一標(biāo)識(shí)
    android:permission="com.example.gyh.Provider"http://訪問權(quán)限
    android:process=":provider"http://ContentProvider運(yùn)行在單獨(dú)的進(jìn)程中
    android:readPermission="com.example.gyh.Provider.read"http://讀取權(quán)限
    android:writePermission="com.example.gyh.Provider.write" //寫權(quán)限   />

建一個(gè)Activity

Uri uri = Uri.parse("content://com.example.gyh.myapplication.ContentProvider_text.BookProvider");這就是xml里生命的唯一標(biāo)識(shí)符
getContentResolver().query(uri, null, null, null, null);
getContentResolver().query(uri, null, null, null, null);
getContentResolver().query(uri, null, null, null, null);

我們可以看一下輸出
12-08 14:05:57.775 22494-22506/com.example.gyh.myapplication:provider I/Provider: query Binder_2
12-08 14:05:57.775 22494-22505/com.example.gyh.myapplication:provider I/Provider: query Binder_1
12-08 14:05:57.775 22494-22506/com.example.gyh.myapplication:provider I/Provider: query Binder_2

每次線程都不一樣,因?yàn)檫@些方法是運(yùn)作在Binder線程池里的 除了onCreate是運(yùn)行在主線程里现使,所以在onCreate是不能進(jìn)行耗時(shí)操作的低匙。


這樣簡(jiǎn)單的ContentProvider就使用成功了,但是為了更好是使用我們需要
結(jié)合SqliteOpenHelper去創(chuàng)建數(shù)據(jù)庫(kù)去存儲(chǔ)數(shù)據(jù),所以我們又建立了類去繼承SqliteOpenHelper去創(chuàng)建數(shù)據(jù)庫(kù)

public class DbOpenHelper extends SQLiteOpenHelper {

    private static final String DB_NAME = "book_provider.db";//數(shù)據(jù)庫(kù)名
    public static final String BOOK_TABLE_NAME = "book";//數(shù)據(jù)表名
    public static final String USER_TALBE_NAME = "user";

    private static final int DB_VERSION = 3;//版本號(hào)

    private String CREATE_BOOK_TABLE = "CREATE TABLE IF NOT EXISTS "
            + BOOK_TABLE_NAME + "(_id INTEGER PRIMARY KEY," + "name TEXT)";

    private String CREATE_USER_TABLE = "CREATE TABLE IF NOT EXISTS "
            + USER_TALBE_NAME + "(_id INTEGER PRIMARY KEY," + "name TEXT,"
            + "sex INT)";

    public DbOpenHelper(Context context) {
        super(context, DB_NAME, null, DB_VERSION);
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(CREATE_BOOK_TABLE);//建表
        db.execSQL(CREATE_USER_TABLE);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    //用于版本更換的時(shí)候調(diào)用
    }

然后我們?cè)趯?duì)ContentProvider進(jìn)行修改

public class BookProvider extends ContentProvider {
    String TAG = "Provider";
    static String AUTHORITY = "com.example.gyh.myapplication.ContentProvider_text.BookProvider";
    private static final UriMatcher sUriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    public static final int BOOK_URI_CODE = 0;
    public static final int USER_URI_CODE = 1;

    static {
        sUriMatcher.addURI(AUTHORITY, "book", BOOK_URI_CODE);
        sUriMatcher.addURI(AUTHORITY, "user", USER_URI_CODE);

    }

    Context mContext;
    SQLiteDatabase mDb;


    @Override
    //可以進(jìn)行一些初始化 該方法運(yùn)行在主線程里朴下,其他五個(gè)方法運(yùn)行在Binder線程池里
    public boolean onCreate() {
        mContext = getContext();
        initProviderData();
        return true;
    }

    private void initProviderData() {
        mDb = new DbOpenHelper(mContext).getWritableDatabase();
        mDb.execSQL("delete from " + DbOpenHelper.BOOK_TABLE_NAME);
        mDb.execSQL("delete from " + DbOpenHelper.USER_TALBE_NAME);
        mDb.execSQL("insert into book values(3,'Android');");
        mDb.execSQL("insert into book values(4,'Ios');");
        mDb.execSQL("insert into book values(5,'Html5');");
    }

    @Nullable
    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        Log.i(TAG, "query " + Thread.currentThread().getName());
        String table = getTableName(uri);
        if (table == null) {
            throw new IllegalArgumentException("Unsupported URI: " + uri);
        }
        return mDb.query(table, projection, selection, selectionArgs, null, null, sortOrder, null);
    }

    @Nullable
    @Override
    //用于返回Uri請(qǐng)求對(duì)于的MIME類型(媒體類型)
    public String getType(Uri uri) {
        Log.i(TAG, "getType " + Thread.currentThread().getName());
        return null;
    }

    private String getTableName(Uri uri) {
        String tableName = null;
        switch (sUriMatcher.match(uri)) {
            case BOOK_URI_CODE:
                tableName = DbOpenHelper.BOOK_TABLE_NAME;
                break;
            case USER_URI_CODE:
                tableName = DbOpenHelper.USER_TALBE_NAME;
                break;
            default:
                break;
        }

        return tableName;
    }

    @Nullable
    @Override
    public Uri insert(Uri uri, ContentValues values) {
        Log.i(TAG, "insert " + Thread.currentThread().getName());
        String tablename = getTableName(uri);
        if (tablename == null) {
            throw new IllegalArgumentException("Unsupported URI: " + uri);
        }
        Log.i(TAG, "insert table name " + tablename);

        mDb.insert(tablename, null, values);
        mContext.getContentResolver().notifyChange(uri, null);
        return uri;
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        String table = getTableName(uri);
        if (table == null) {
            throw new IllegalArgumentException("Unsupported URI: " + uri);
        }
        int count = mDb.delete(table, selection, selectionArgs);
        if (count > 0) {
            getContext().getContentResolver().notifyChange(uri, null);
        }
        return count;
    }

    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        Log.i(TAG, "update " + Thread.currentThread().getName());
        String table = getTableName(uri);
        if (table == null) {
            throw new IllegalArgumentException("Unsupported URI: " + uri);
        }
        int row = mDb.update(table, values, selection, selectionArgs);
        if (row > 0) {
            getContext().getContentResolver().notifyChange(uri, null);
        }
        return row;
    }
}

對(duì)Activity進(jìn)行修改

Uri bookUri = Uri.parse("content://com.example.gyh.myapplication.ContentProvider_text.BookProvider/book");
ContentValues values = new ContentValues();
values.put("_id", 6);
values.put("name", "程序設(shè)計(jì)的藝術(shù)");
getContentResolver().insert(bookUri, values);
Cursor bookCursor = getContentResolver().query(bookUri, new String[]{"_id", "name"}, null, null, null);
while (bookCursor.moveToNext()) {//這里的0指的是篩選的第一個(gè)條件就是_id
    Log.d(TAG, "query book:" + bookCursor.getInt(0) + "  " + bookCursor.getString(1));
}
bookCursor.close();

意思代碼完成了ContentProvider的基本使用


對(duì)上述代碼中可能大家會(huì)對(duì)Uri和UriMatcher的使用不是很了解 那么我來舉個(gè)例子
第一部初始化
UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH);
第二部將Uri和UriMatcher配對(duì)

matcher.addURI("com.yfz.Lesson", "person/#", PEOPLE_ID);  PEOPLE_ID是code int```
第三步我們就可以通過請(qǐng)求的Uri進(jìn)行操作
```Uri uri = Uri.parse("content://" + "com.yfz.Lesson" + "/people");  
int match = matcher.match(uri);  
       switch (match)  
       {  
           case PEOPLE:  
               return "vnd.android.cursor.dir/people";  
           case PEOPLE_ID:  
               return "vnd.android.cursor.item/people";  
           default:  
               return null;  
       }  ```
返回的結(jié)果就是"vnd.android.cursor.dir/person".

我們可以看到UriMatcher作用是可以組合Uri 這里將com.yfz.Lesson和people結(jié)合content://com.yfz.Lesson/people 所以我們后面輸入的Uri不再是content://com.yfz.Lesson而是content://com.yfz.Lesson/people努咐,我們可以通過輸入的Uri判斷出他們的Code,進(jìn)行一些判斷和操作殴胧。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末渗稍,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子团滥,更是在濱河造成了極大的恐慌竿屹,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件灸姊,死亡現(xiàn)場(chǎng)離奇詭異拱燃,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)力惯,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門碗誉,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人父晶,你說我怎么就攤上這事哮缺。” “怎么了甲喝?”我有些...
    開封第一講書人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵尝苇,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我埠胖,道長(zhǎng)糠溜,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任直撤,我火速辦了婚禮非竿,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘谋竖。我一直安慰自己红柱,他們只是感情好侮东,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著豹芯,像睡著了一般。 火紅的嫁衣襯著肌膚如雪驱敲。 梳的紋絲不亂的頭發(fā)上铁蹈,一...
    開封第一講書人閱讀 49,007評(píng)論 1 284
  • 那天,我揣著相機(jī)與錄音众眨,去河邊找鬼握牧。 笑死,一個(gè)胖子當(dāng)著我的面吹牛娩梨,可吹牛的內(nèi)容都是我干的沿腰。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼狈定,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼颂龙!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起纽什,我...
    開封第一講書人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤措嵌,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后芦缰,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體企巢,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年让蕾,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了浪规。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡探孝,死狀恐怖笋婿,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情再姑,我是刑警寧澤萌抵,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布,位于F島的核電站元镀,受9級(jí)特大地震影響绍填,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜栖疑,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一讨永、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧遇革,春花似錦卿闹、人聲如沸揭糕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽著角。三九已至,卻和暖如春旋恼,著一層夾襖步出監(jiān)牢的瞬間吏口,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來泰國(guó)打工冰更, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留产徊,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓蜀细,卻偏偏與公主長(zhǎng)得像舟铜,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子奠衔,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容