ViewModel、LiveData淺析(MVVM)

由于MVP固有的一些因素,比如隨著項(xiàng)目越來越大P層越來越膨脹和內(nèi)存泄露問題,MVVM就面世了。MVVM組成:Model,ViewModel,View,其實(shí)和MVP差不多,就是把Presenter換成了VIewModel。簡單的概括關(guān)系就是:
M --> VM --> V,
M被VM持有,由VM來處理業(yè)務(wù)邏輯,而VM又被V持有,實(shí)際上是形成一條單向線的。不過VM和V會通過binder來實(shí)現(xiàn)雙向綁定,這樣也就進(jìn)一步達(dá)到了解耦的目的。配上一張看到的很形象的圖:


mvvm-arch.jpg

由于谷歌爸爸提供的JetPack庫,我們可以很容易的拿到相應(yīng)的組件來實(shí)現(xiàn)。簡單的提一下相關(guān)的組件:
ViewModel、LiveData/DataBinding、

ViewModel

作為核心成員,實(shí)際上是一個抽象類:


ViewModel.jpg
/**
 * ViewModel is a class that is responsible for preparing and managing the data for
 * an {@link android.app.Activity Activity} or a {@link androidx.fragment.app.Fragment Fragment}.
 * It also handles the communication of the Activity / Fragment with the rest of the application
 * (e.g. calling the business logic classes).
 * <p>
 * A ViewModel is always created in association with a scope (an fragment or an activity) and will
 * be retained as long as the scope is alive. E.g. if it is an Activity, until it is
 * finished.
 * <p>
 * In other words, this means that a ViewModel will not be destroyed if its owner is destroyed for a
 * configuration change (e.g. rotation). The new instance of the owner will just re-connected to the
 * existing ViewModel.
 * <p>
 * The purpose of the ViewModel is to acquire and keep the information that is necessary for an
 * Activity or a Fragment. The Activity or the Fragment should be able to observe changes in the
 * ViewModel. ViewModels usually expose this information via {@link LiveData} or Android Data
 * Binding. You can also use any observability construct from you favorite framework.
 * <p>
 * ViewModel's only responsibility is to manage the data for the UI. It <b>should never</b> access
 * your view hierarchy or hold a reference back to the Activity or the Fragment.
 * <p>
 * ViewModels can also be used as a communication layer between different Fragments of an Activity.
 * Each Fragment can acquire the ViewModel using the same key via their Activity. This allows
 * communication between Fragments in a de-coupled fashion such that they never need to talk to
 * the other Fragment directly.
* <p>
 */
public abstract class ViewModel {
        ...
}

經(jīng)典的注解更重要系列,甚至連用法都告訴我們了但太長被我截了(不自覺得跪在了地上)。
注解:

1.ViewModel是一個類,負(fù)責(zé)為Activity或Fragment準(zhǔn)備和管理數(shù)據(jù)。它還處理Activity/Fragment與應(yīng)用程序其余部分的通信(例如,調(diào)用業(yè)務(wù)邏輯類)。

2.ViewModel始終與范圍(片段或活動)相關(guān)聯(lián)地創(chuàng)建,并且只要范圍是活動的,就將被保留。例如。如果是活動,則直到完成。換句話說,這意味著如果ViewModel的所有者因配置更改(例如旋轉(zhuǎn))而被銷毀,則不會銷毀它。所有者的新實(shí)例將重新連接到現(xiàn)有的ViewModel。

3.ViewModel的目的是獲取并保留Activity或Fragment所需的信息。Activity或Fragment應(yīng)該能夠觀察 ViewModel中的更改。 ViewModel通常通過{@link LiveData}或Android Data Binding公開此信息。您也可以使用自己喜歡的框架中的任何可觀察性構(gòu)造。

4.ViewModel的唯一責(zé)任是管理UI的數(shù)據(jù)。它絕不能訪問您的視圖層次結(jié)構(gòu)或保留對活動或片段的引用。

5.ViewModels也可以用作Activity的不同F(xiàn)ragment之間的通信層。每個Fragment都可以通過其Activity使用相同的鍵獲取ViewModel。這允許Fragment之間以解耦的方式進(jìn)行通信,從而使它們無需直接與另一個片段交談。

這樣看來,ViewModel的職責(zé)就很清晰了,就是替Activity或Fragment管理數(shù)據(jù)的,也就是前面說的相當(dāng)于Presenter,用來處理業(yè)務(wù)邏輯,所以本身并沒有很神秘。注意一下第三點(diǎn),View層是應(yīng)該能觀察到ViewModel中的數(shù)據(jù)更改的,可以通過LiveData、DataBinding或者其他的觀察者模式框架來實(shí)現(xiàn),這也就是View和ViewModel實(shí)現(xiàn)雙向綁定的方法。

附一張作用域圖:


ViewModelScope.png

簡單使用:

public class UserModel extends ViewModel {

    @Override
    protected void onCleared() {
        super.onCleared();
        //do cleared work
    }

    void doAction(){
        // depending on the action, do necessary business logic calls 
        // and update the userLiveData.
    }
}

還是借鑒一下官網(wǎng)的例子:

public class MyViewModel extends ViewModel {
    private MutableLiveData<List<User>> users;
    public LiveData<List<User>> getUsers() {
        if (users == null) {
            users = new MutableLiveData<List<User>>();
            loadUsers();
        }
        return users;
    }

    private void loadUsers() {
        // Do an asynchronous operation to fetch users.
        //這里就可以做一些異步操作來獲取數(shù)據(jù)
    }
}
public class MyActivity extends AppCompatActivity {
    public void onCreate(Bundle savedInstanceState) {
        // Create a ViewModel the first time the system calls an activity's onCreate() method.
        // Re-created activities receive the same MyViewModel instance created by the first activity.

        MyViewModel model = new ViewModelProvider(this).get(MyViewModel.class);
        model.getUsers().observe(this, users -> {
            // update UI
        });
    }
}

就是這么簡單,就把界面和業(yè)務(wù)邏輯分離了,這里用到了LiveData,后面再說。重寫onCleared()做一些資源的釋放,以及我們自己提供一些業(yè)務(wù)邏輯的操作。onCleared()會在Activity/Fragment被回收時調(diào)用,這是怎么做到的呢?

先看下ViewModel是怎么獲取的:

UserModel userModel = new ViewModelProvider(this, new UserModelFactory()).get(UserModel.class);

引入implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'庫則可以使用單參數(shù)構(gòu)造器:

UserModel userModel = new ViewModelProvider(this).get(UserModel.class);

通過ViewModelProvider的get()方法來獲取,先看下這個ViewModelProvider


ViewModelProvider.png

這里要注意的是androidx.lifecycle:lifecycle-viewmodel:2.2.0包才提供了單參數(shù)的構(gòu)造方法。

get()方法:

    @NonNull
    @MainThread
    public <T extends ViewModel> T get(@NonNull Class<T> modelClass) {
        String canonicalName = modelClass.getCanonicalName();
        if (canonicalName == null) {
            throw new IllegalArgumentException("Local and anonymous classes can not be ViewModels");
        }
        return get(DEFAULT_KEY + ":" + canonicalName, modelClass);
    }

    @NonNull
    @MainThread
    public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {
        ViewModel viewModel = mViewModelStore.get(key);

        if (modelClass.isInstance(viewModel)) {
            //noinspection unchecked
            return (T) viewModel;
        } else {
            //noinspection StatementWithEmptyBody
            if (viewModel != null) {
                // TODO: log a warning.
            }
        }
        if (mFactory instanceof KeyedFactory) {
            viewModel = ((KeyedFactory) (mFactory)).create(key, modelClass);
        } else {
            viewModel = (mFactory).create(modelClass);
        }
        mViewModelStore.put(key, viewModel);
        //noinspection unchecked
        return (T) viewModel;
    }

可以看到,先根據(jù)類名和DEFAULT_KEY進(jìn)行拼裝得到key,然后在去mViewModelStore里取。如果取出來的viewModel是我們要的類,那就直接返回(此時viewModel可能為null),否則就“l(fā)og a warning”。

接著判斷持有的mFactory是否是KeyedFactory的類型,如果是就通過這個Factory來構(gòu)造出viewModel,如果是別的類型,就直接create。然后把viewModel再放進(jìn)mViewModelStore里,其實(shí)這里也就表明了,如果不同的Activity/Fragment去獲取同類型的ViewModel的話,那么取到的實(shí)例(引用)就是一樣的,那當(dāng)然持有的數(shù)據(jù)就是一致的,也就能達(dá)到通信的效果。

而ViewModelProvider之所以為我們提供了Factory,就是因?yàn)樵讷@取ViewModel的時候我們是不能直接通過new來用構(gòu)造器創(chuàng)建的,這樣的話獲取的都是全新的ViewModel對象,那就沒有意義了。所以由Factory來提供,并可以讓我們創(chuàng)建有參的ViewModel。(這里保留觀點(diǎn),待核實(shí))

看下ViewModelStore:

public class ViewModelStore {

    private final HashMap<String, ViewModel> mMap = new HashMap<>();

    final void put(String key, ViewModel viewModel) {
        ViewModel oldViewModel = mMap.put(key, viewModel);
        if (oldViewModel != null) {
            oldViewModel.onCleared();
        }
    }

    final ViewModel get(String key) {
        return mMap.get(key);
    }

    Set<String> keys() {
        return new HashSet<>(mMap.keySet());
    }

    /**
     *  Clears internal storage and notifies ViewModels that they are no longer used.
     */
    public final void clear() {
        for (ViewModel vm : mMap.values()) {
            vm.clear();
        }
        mMap.clear();
    }
}

很簡單,通過HashMap來存ViewModel,注意一下clear()方法,會將持有的VM都進(jìn)行清理

    @MainThread
    final void clear() {
        mCleared = true;
        // Since clear() is final, this method is still called on mock objects
        // and in those cases, mBagOfTags is null. It'll always be empty though
        // because setTagIfAbsent and getTag are not final so we can skip
        // clearing it
        if (mBagOfTags != null) {
            synchronized (mBagOfTags) {
                for (Object value : mBagOfTags.values()) {
                    // see comment for the similar call in setTagIfAbsent
                    closeWithRuntimeException(value);
                }
            }
        }
        onCleared();
    }

可以看到clear()內(nèi)部就調(diào)用了給我們自己實(shí)現(xiàn)的onCleared()方法,到這里我們只知道邏輯,但還是不知道調(diào)用時機(jī)?;剡^頭來看下ViewModelProvider的構(gòu)造函數(shù):

public ViewModelProvider(@NonNull ViewModelStoreOwner owner, @NonNull Factory factory) {
        this(owner.getViewModelStore(), factory);
    }

public ViewModelProvider(@NonNull ViewModelStore store, @NonNull Factory factory) {
        mFactory = factory;
        mViewModelStore = store;
    }

提供了兩種構(gòu)造函數(shù),但最終都是通過ViewModelStore和Factory來構(gòu)造的。那這個ViewModelStoreOwner又是什么呢,其實(shí)看名字就應(yīng)該知道,就是用來提供ViewModelStore的。

public interface ViewModelStoreOwner {
    /**
     * Returns owned {@link ViewModelStore}
     *
     * @return a {@code ViewModelStore}
     */
    @NonNull
    ViewModelStore getViewModelStore();
}

實(shí)際上,就是一個接口??聪滤膶?shí)現(xiàn)類,就是ComponentActivity:

    @Override
    public ViewModelStore getViewModelStore() {
        if (getApplication() == null) {
            throw new IllegalStateException("Your activity is not yet attached to the "
                    + "Application instance. You can't request ViewModel before onCreate call.");
        }
        if (mViewModelStore == null) {
            NonConfigurationInstances nc =
                    (NonConfigurationInstances) getLastNonConfigurationInstance();
            if (nc != null) {
                // Restore the ViewModelStore from NonConfigurationInstances
                mViewModelStore = nc.viewModelStore;
            }
            if (mViewModelStore == null) {
                mViewModelStore = new ViewModelStore();
            }
        }
        return mViewModelStore;
    }

重寫了該方法,目的也就是為了提供mViewModelStore。再看下ComponentActivity的繼承關(guān)系:


ComponentActivity.png

這樣就很清晰了,也就是說我們的Activity也實(shí)現(xiàn)了ViewModelStoreOwner接口,再看下ComponentActivity的構(gòu)造函數(shù):

public ComponentActivity() {
        Lifecycle lifecycle = getLifecycle();
        //noinspection ConstantConditions
        if (lifecycle == null) {
            throw new IllegalStateException("getLifecycle() returned null in ComponentActivity's "
                    + "constructor. Please make sure you are lazily constructing your Lifecycle "
                    + "in the first call to getLifecycle() rather than relying on field "
                    + "initialization.");
        }
      
        getLifecycle().addObserver(new LifecycleEventObserver() {
            @Override
            public void onStateChanged(@NonNull LifecycleOwner source,
                    @NonNull Lifecycle.Event event) {
                if (event == Lifecycle.Event.ON_DESTROY) {
                    if (!isChangingConfigurations()) {
                        getViewModelStore().clear();
                    }
                }
            }
        });
    }

這里刪減了其他不相關(guān)的代碼,可以看到,持有了Lifecycle實(shí)例,通過對生命周期的監(jiān)聽(這里是ON_DESTROY),來實(shí)現(xiàn)clear邏輯。這里還要注意的一點(diǎn)是,當(dāng)配置改變的時候(翻轉(zhuǎn)),是不會去清理的。這也很好理解,因?yàn)橹皇墙缑嬷乩L,數(shù)據(jù)如果重新獲?。╒iewModel)那還是一樣的數(shù)據(jù)。

到這里,為什么ViewModel能在Activity被回收時去清理資源就很清晰了。

LiveData

上面也有提到,其實(shí)真正存儲數(shù)據(jù)的是LiveData。先從用法入手:

      userModel.liveData.observe(this, new Observer<Integer>() {
            @Override
            public void onChanged(Integer integer) {
                textView.setText(String.valueOf(integer));
            }
        });

LiveData由ViewModel來持有,為其添加觀察者并指定動作則可以監(jiān)聽數(shù)據(jù),當(dāng)數(shù)據(jù)變化時我們就可以更新UI或者做其他的事情,經(jīng)典的觀察者模式。

public abstract class LiveData<T> {
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    final Object mDataLock = new Object();
    static final int START_VERSION = -1;
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    static final Object NOT_SET = new Object();

    private SafeIterableMap<Observer<? super T>, ObserverWrapper> mObservers =
            new SafeIterableMap<>();

    // how many observers are in active state
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    int mActiveCount = 0;
    private volatile Object mData;
    // when setData is called, we set the pending data and actual data swap happens on the main
    // thread
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    volatile Object mPendingData = NOT_SET;
    private int mVersion;

    private boolean mDispatchingValue;
    @SuppressWarnings("FieldCanBeLocal")
    private boolean mDispatchInvalidated;

    private final Runnable mPostValueRunnable = new Runnable() {
        @SuppressWarnings("unchecked")
        @Override
        public void run() {
            Object newValue;
            synchronized (mDataLock) {
                newValue = mPendingData;
                mPendingData = NOT_SET;
            }
            setValue((T) newValue);
        }
    };

    /**
     * Creates a LiveData initialized with the given {@code value}.
     *
     * @param value initial value
     */
    public LiveData(T value) {
        mData = value;
        mVersion = START_VERSION + 1;
    }

    /**
     * Creates a LiveData with no value assigned to it.
     */
    public LiveData() {
        mData = NOT_SET;
        mVersion = START_VERSION;
    }
}

注釋比較長就沒貼了,大致如下:

1.LiveData是可以在給定生命周期內(nèi)觀察到的數(shù)據(jù)持有者類。這意味著可以將{@link Observer}與{@link LifecycleOwner}成對添加,并且只有配對的LifecycleOwner處于活動狀態(tài)時,才會向該觀察者通知有關(guān)包裝數(shù)據(jù)的修改。如果LifecycleOwner的狀態(tài)為{@link Lifecycle.StateSTARTED}或{@link Lifecycle.StateRESUMED},則將其視為活動狀態(tài)。通過{@link watchForever(Observer)}添加的觀察者被視為始終處于活動狀態(tài),因此將始終收到有關(guān)修改的通知。對于這些觀察者,您應(yīng)該手動調(diào)用{@link removeObserver(Observer)}。

2.如果相應(yīng)的生命周期變?yōu)閧@link Lifecycle.StateDESTROYED}狀態(tài),則添加了生命周期的觀察者將被自動刪除。這對于活動和片段可以安全地觀察LiveData而不用擔(dān)心泄漏的活動特別有用:銷毀它們時,它們將立即被取消訂閱。

3.此外,LiveData具有{@link LiveData#onActive()}和{@link LiveData#onInactive()}方法,可在活動{@link Observer}的數(shù)量在0到1之間變化時得到通知。這就允許LiveData可以釋放大量的資源,當(dāng)沒有任何正在觀察的觀察者時。

4.此類旨在容納{@link ViewModel}的各個數(shù)據(jù)字段,但也可以用于以解耦的方式在應(yīng)用程序中的不同模塊之間共享數(shù)據(jù)。

幾個重要的字段:
mDataLock:設(shè)值時的鎖
START_VERSION:數(shù)據(jù)(消息)起始版本
mObservers:觀察者集合SafeIterableMap,其實(shí)是由雙向鏈表來偽裝成的Map
mData:持有的數(shù)據(jù)
mVersion:當(dāng)前數(shù)據(jù)版本
mPostValueRunnable:提供設(shè)值邏輯

有兩個構(gòu)造函數(shù),帶值的就直接將值賦給mData,并將起始版本號加1(因?yàn)橐呀?jīng)有數(shù)據(jù)了嘛);無參構(gòu)造函數(shù)就正常初始化。

我們就先從observe()方法看起,這也是第一條注釋提到的,可以將Observer(觀察者)和LifecycleOwner(生命周期持有者)成對添加。

   /**
    * @param owner    The LifecycleOwner which controls the observer
     * @param observer The observer that will receive the events
     */
    @MainThread
    public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
        assertMainThread("observe");
        if (owner.getLifecycle().getCurrentState() == DESTROYED) {
            // ignore
            return;
        }
        LifecycleBoundObserver wrapper = new LifecycleBoundObserver(owner, observer);
        ObserverWrapper existing = mObservers.putIfAbsent(observer, wrapper);
        if (existing != null && !existing.isAttachedTo(owner)) {
            throw new IllegalArgumentException("Cannot add the same observer"
                    + " with different lifecycles");
        }
        if (existing != null) {
            return;
        }
        owner.getLifecycle().addObserver(wrapper);
    }

從名字也可以看出,這個方法就是用來添加(注冊)觀察者的,而LifecycleOwner則是對應(yīng)的觀察者的生命周期持有者,是一個有關(guān)生命周期的接口,由ComponentActivity實(shí)現(xiàn)。目的很簡單,匿名內(nèi)部類持有外部類引發(fā)的內(nèi)存泄漏,以及當(dāng)我們頁面都已經(jīng)關(guān)閉了,那么再來接收事件(更新界面)那就沒有意義了。

首先assertMainThread()方法就是來判斷當(dāng)前是否為主線程,如果不是就拋出異常(這里因該是持有了LifecycleOwner的原因)。接著判斷當(dāng)前LifecycleOwner的狀態(tài),如果是DESTROYED狀態(tài)就直接return掉。

緊接著封裝了一個LifecycleBoundObserver對象,主要提供了解綁Observer方法,然后添加到mObservers中。如果觀察者已經(jīng)存在并且綁定到了owner,那就拋出異常。

最后拿到Lifecycle(抽象類,實(shí)現(xiàn)類為LifecycleRegistry)對象,調(diào)用addObserver()添加wrapper,這里主要就是監(jiān)聽生命周期狀態(tài)。

還提供了observeForever(),也就是無論生命周期狀態(tài)如何都一直監(jiān)聽,那就需要我們自己去移除監(jiān)聽者,這里就不看了。

接著看下setValue()方法:

   /**
     * Sets the value. If there are active observers, the value will be dispatched to them.
     * <p>
     * This method must be called from the main thread. If you need set a value from a background
     * thread, you can use {@link #postValue(Object)}
     *
     * @param value The new value
     */
    @MainThread
    protected void setValue(T value) {
        assertMainThread("setValue");
        mVersion++;
        mData = value;
        dispatchingValue(null);
    }

設(shè)置值,如果有存活的observer就去分發(fā)。一樣先判斷是否是主線程,將mVersion加1(相當(dāng)于這是新版本的數(shù)據(jù)),將新值賦給mData,進(jìn)行分發(fā):

void dispatchingValue(@Nullable ObserverWrapper initiator) {
        if (mDispatchingValue) {
            mDispatchInvalidated = true;
            return;
        }
        mDispatchingValue = true;
        do {
            mDispatchInvalidated = false;
            if (initiator != null) {
                considerNotify(initiator);
                initiator = null;
            } else {
                for (Iterator<Map.Entry<Observer<? super T>, ObserverWrapper>> iterator =
                        mObservers.iteratorWithAdditions(); iterator.hasNext(); ) {
                    considerNotify(iterator.next().getValue());
                    if (mDispatchInvalidated) {
                        break;
                    }
                }
            }
        } while (mDispatchInvalidated);
        mDispatchingValue = false;
    }

在setValue()方法中調(diào)用dispatchingValue()時傳入了null,所以先看initiator為null的情況。很簡單,就是拿到mObservers的迭代器,然后調(diào)用considerNotify()方法判斷這個Observer是否要通知,看下considerNotify()方法:

private void considerNotify(ObserverWrapper observer) {
        if (!observer.mActive) {
            return;
        }
        // Check latest state b4 dispatch. Maybe it changed state but we didn't get the event yet.
        //
        // we still first check observer.active to keep it as the entrance for events. So even if
        // the observer moved to an active state, if we've not received that event, we better not
        // notify for a more predictable notification order.
        if (!observer.shouldBeActive()) {
            observer.activeStateChanged(false);
            return;
        }
        if (observer.mLastVersion >= mVersion) {
            return;
        }
        observer.mLastVersion = mVersion;
        observer.mObserver.onChanged((T) mData);
    }

首先還是先判斷了observer是否是Active狀態(tài),如果不是就調(diào)用observer.activeStateChanged(false)去更新狀態(tài);
接著判斷observer的mLastVersion是否大于或等于LiveData的mVersion,如果是就直接return。也就是說,數(shù)據(jù)(消息)的版本(mVersion)要大于監(jiān)聽者的版本(mLastVersion )時,才會去做數(shù)據(jù)的分發(fā)通知,我們只對新版本的數(shù)據(jù)感興趣(也可以把mVersion當(dāng)做最新的消息版本,把mLastVersion當(dāng)做上一次的消息版本,只有新消息的版本大于上一次消息的版本,我們才去通知)。其實(shí)這里也是引發(fā)粘性事件的地方,EventBus也是。往回看就可以知道,不管是observer.mLastVersion還是mVersion,它們的起始值都是START_VERSION(-1),而在setValue()的時候,mVersion進(jìn)行了自加,也就是第一次的時候就變?yōu)榱?,而mLastVersion并沒有改動。那么這就導(dǎo)致了,即使是后于設(shè)值注冊的observer,走到這里的時候,mLastVersion(-1)是小于mVersion(0)的,這也就讓數(shù)據(jù)正常的往下走通知了出去。這也是為什么我們在使用EventBus去發(fā)消息的時候,明明消息已經(jīng)發(fā)出去了,而新開啟的Activity注冊之后仍然能接收到的原因。當(dāng)然這點(diǎn)也可以利用來作為Activity間開啟的數(shù)據(jù)傳遞。

接著就是將mVersion賦給mLastVersion,告訴observer:這個數(shù)據(jù)已經(jīng)舊了,下次要獲取就要獲取新的。然后回調(diào)observer的onChanged()方法把數(shù)據(jù)更新出去,這也就是一開始調(diào)用observe()來添加observer需要重寫的方法。

后記

不知不覺,這篇文章居然在草稿箱里躺了至少快兩年,當(dāng)初是想說感覺寫的還不夠成熟想在完善一下再發(fā),沒想到一擱置就這么久...懶真的是要命啊啊啊ε=ε=ε=(#>д<)?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容