MVVM 架構演進(二) —— DataBinding 實現原理

前言

我們知道 DataBinding 在 MVVM 架構中起到了不可或缺的作用, 它是削弱 View 層與 ViewModel 之間耦合的重中之重, 那麼它是如何做到當數據變更時便能夠直接推送到 View 中的呢?

我們帶着這個問題去探索一下 DataBinding 的工作流程.

一. ViewDataBinding 的創建

class MainFragment : Fragment() {

    ......
    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        // 調用了 MainFragmentBinding.inflate 獲取到了 MainFragmentBinding 對象
        val dataBinding: MainFragmentBinding = MainFragmentBinding.inflate(inflater, container, false)
        dataBinding.view = this
        dataBinding.viewmodel = ViewModelProviders.of(this).get(MainViewModel::class.java)
        return dataBinding.root
    }

}

ViewBinding 的實例化是通過生成類 MainFragmentBinding.inflate 完成的, 我們繼續追蹤

public abstract class MainFragmentBinding extends ViewDataBinding {
  
  @NonNull
  public static MainFragmentBinding inflate(@NonNull LayoutInflater inflater,
      @Nullable ViewGroup root, boolean attachToRoot) {
    // 1. 調用 DataBindingUtil.getDefaultComponent 獲取了默認的 DataBindingComponent 對象, 爲空
    // 調用了重載方法
    return inflate(inflater, root, attachToRoot, DataBindingUtil.getDefaultComponent());
  }

  @NonNull
  public static MainFragmentBinding inflate(@NonNull LayoutInflater inflater,
      @Nullable ViewGroup root, boolean attachToRoot, @Nullable DataBindingComponent component) {
    // 將任務分發給 DataBindingUtil.inflate
    return DataBindingUtil.<MainFragmentBinding>inflate(
             inflater,
             com.sharry.demo.mvvm.R.layout.main_fragment, 
             root, 
             attachToRoot, 
             component
           );
  }
    
}

好的, 可以看到 MainFragmentBinding.inflate 會轉交給 DataBindUtil 去執行 inflate 操作, 這裏已經可以看到這個 ViewDataBinding 對應的 xml 佈局文件了

接下來我們看看這個 DataBindUtil.inflate 做了什麼操作

public class DataBindingUtil {
    
    public static <T extends ViewDataBinding> T inflate(
            @NonNull LayoutInflater inflater, int layoutId, @Nullable ViewGroup parent,
            boolean attachToParent, @Nullable DataBindingComponent bindingComponent) {
        // 判斷是否作爲子孩子填充到父容器
        final boolean useChildren = parent != null && attachToParent;
        // 判斷插入起始位置
        final int startChildren = useChildren ? parent.getChildCount() : 0;
        // 1. 構造視圖
        final View view = inflater.inflate(layoutId, parent, attachToParent);
        // 2. 執行視圖的綁定
        if (useChildren) {
            // 與新插入的的視圖進行綁定
            return bindToAddedViews(bindingComponent, parent, startChildren, layoutId);
        } else {
            // 與新創建的 View 進行綁定
            return bind(bindingComponent, view, layoutId);
        }
    }
    
    private static <T extends ViewDataBinding> T bindToAddedViews(DataBindingComponent component,
            ViewGroup parent, int startChildren, int layoutId) {
        // 記錄當前父容器子 View 的數量
        final int endChildren = parent.getChildCount();
        // 計算新插入的數量(一般爲 1, 但新插入的視圖爲 <merge></merge> 標籤就不好說了)
        final int childrenAdded = endChildren - startChildren;
        if (childrenAdded == 1) {
            // 2.1 與單 view 進行綁定
            final View childView = parent.getChildAt(endChildren - 1);
            return bind(component, childView, layoutId);
        } else {
            // 2.2 與多 view 進行綁定
            final View[] children = new View[childrenAdded];
            for (int i = 0; i < childrenAdded; i++) {
                children[i] = parent.getChildAt(i + startChildren);
            }
            return bind(component, children, layoutId);
        }
    }
    
    private static DataBinderMapper sMapper = new DataBinderMapperImpl();
    
    // 執行多 view 的綁定
    static <T extends ViewDataBinding> T bind(DataBindingComponent bindingComponent, View[] roots,
            int layoutId) {
        return (T) sMapper.getDataBinder(bindingComponent, roots, layoutId);
    }

    // 執行單 View 的綁定
    static <T extends ViewDataBinding> T bind(DataBindingComponent bindingComponent, View root,
            int layoutId) {
        return (T) sMapper.getDataBinder(bindingComponent, root, layoutId);
    }
    
}

好的, 可以看到 DataBindingUtil.inflate 的主要操作有兩個

  • 實例化 xml 佈局爲 View
  • 對新的 view 構建執行綁定, 獲取其對應的 ViewDataBinding 對象

好的, 接下來我們看看他是如何獲取 ViewDataBinding 的

// 這個 DataBinderMapperImpl 是 AndroidX 依賴包中的類
public class DataBinderMapperImpl extends MergedDataBinderMapper {
  DataBinderMapperImpl() {
    // 這個 DataBinderMapperImpl 是編譯時的生成類
    addMapper(new com.sharry.demo.mvvm.DataBinderMapperImpl());
  }
}

public class MergedDataBinderMapper extends DataBinderMapper {
    
    private List<DataBinderMapper> mMappers = new CopyOnWriteArrayList<>();

    @Override
    public ViewDataBinding getDataBinder(DataBindingComponent bindingComponent, View view,
            int layoutId) {
        // 在 DataBinderMapperImpl 中添加了一個 DataBinderMapperImpl 對象
        for(DataBinderMapper mapper : mMappers) {
            // 因此這裏調用的是 DataBinderMapperImpl.getDataBinder
            ViewDataBinding result = mapper.getDataBinder(bindingComponent, view, layoutId);
            if (result != null) {
                return result;
            }
        }
        ......
        return null;
    }

}

public class DataBinderMapperImpl extends DataBinderMapper {

  private static final int LAYOUT_MAINFRAGMENT = 1;
  private static final SparseIntArray INTERNAL_LAYOUT_ID_LOOKUP = new SparseIntArray(1);

  static {
    // 靜態代碼塊中, 添加了我們使用了 databinding 的佈局
    INTERNAL_LAYOUT_ID_LOOKUP.put(com.sharry.demo.mvvm.R.layout.main_fragment, LAYOUT_MAINFRAGMENT);
  }

  @Override
  public ViewDataBinding getDataBinder(DataBindingComponent component, View view, int layoutId) {
    int localizedLayoutId = INTERNAL_LAYOUT_ID_LOOKUP.get(layoutId);
    if(localizedLayoutId > 0) {
      // 使用 DataBinding 的 xml 要求使用 <layout></layout> 標籤
      // 因此 DataBinding 的 xml 解析器會爲內部的 view 注入一個 TAG
      final Object tag = view.getTag();
      ......
      switch(localizedLayoutId) {
        case  LAYOUT_MAINFRAGMENT: {
          if ("layout/main_fragment_0".equals(tag)) {
            // 返回了一個 MainFragmentBindingImpl 對象, 這也是編譯時生成的
            return new MainFragmentBindingImpl(component, view);
          }
          throw new IllegalArgumentException("The tag for main_fragment is invalid. Received: " + tag);
        }
      }
    }
    return null;
  }
  
}

好的, 可以看到最終我們獲取到了 MainFragmentBindingImpl 這個對象, 這便是 ViewDataBinding 的實現類了, 它也是編譯時生成的

流程回顧

這裏我們還看沒有看到 View 與 ViewModel 中的數據關聯的過程, 接下來我們順着 MainFragmentBindingImpl 的實例化繼續探索

二. ViewDataBinding 的初始化

public class MainFragmentBindingImpl extends MainFragmentBinding implements com.sharry.demo.mvvm.generated.callback.OnClickListener.Listener {
    
    public MainFragmentBindingImpl(@Nullable androidx.databinding.DataBindingComponent bindingComponent, @NonNull View root) {
        this(bindingComponent, root, mapBindings(bindingComponent, root, 2, sIncludes, sViewsWithIds));
    }
    
    private MainFragmentBindingImpl(androidx.databinding.DataBindingComponent bindingComponent, View root, Object[] bindings) {
        // 將佈局內的視圖添加到父類中暫存
        super(bindingComponent, root, 1
            , (android.widget.Button) bindings[1]
            , (androidx.constraintlayout.widget.ConstraintLayout) bindings[0]
            );
        // 清空 tag
        this.btnCenter.setTag(null);
        this.main.setTag(null);
        // 將根視圖標記爲 databinding 的視圖
        setRootTag(root);
        // 重置數據
        invalidateAll();
    }
    
}

好的, 可以看到 MainFragmentBindingImpl 中保存了我們 xml 的視圖, 並且根據我們的設置創建了監聽器

感覺距離 View 綁定 Observable 又近了一步, 定位到 invalidateAll 這個方法, 我們看看它做了什麼

public class MainFragmentBindingImpl extends MainFragmentBinding implements com.sharry.demo.mvvm.generated.callback.OnClickListener.Listener {
    
    @Override
    public void invalidateAll() {
        synchronized(this) {
                mDirtyFlags = 0x8L;
        }
        requestRebind();
    } 
    
    private static final boolean USE_CHOREOGRAPHER = SDK_INT >= 16;
    
    protected void requestRebind() {
        // 初始化狀態爲 null
        if (mContainingBinding != null) {
            ......
        } else {
            ......
            if (USE_CHOREOGRAPHER) {
                // 投遞到下一幀執行 callback, 最終還是會調用到 mRebindRunnable
                mChoreographer.postFrameCallback(mFrameCallback);
            } else {
                // 通過 Handler 直接 post 到消息隊列
                mUIThreadHandler.post(mRebindRunnable);
            }
        }
    }
}

好的, 可以看到, invalidateAll 操作, 最終會回調 mRebindRunnable, 接下來我們它的實現

public abstract class ViewDataBinding extends BaseObservable {
    
    private final Runnable mRebindRunnable = new Runnable() {
        @Override
        public void run() {
            ......
            executePendingBindings();
        }
    };
   
    public void executePendingBindings() {
        // 判斷當前 ViewDataBinding 是否持有其他的引用, 初始時是沒有的
        if (mContainingBinding == null) {
            // 調用了 executeBindingsInternal 進行後續操作
            executeBindingsInternal();
        } else {
            // ......
        }
    }

    private void executeBindingsInternal() {
        // 若當前存在正在執行的任務, 則將本次操作投遞到下一幀執行
        if (mIsExecutingPendingBindings) {
            requestRebind();
            return;
        }
        ......
        mIsExecutingPendingBindings = true;
        mRebindHalted = false;
        ......// 通知外界, 執行了 rebind 操作
        if (!mRebindHalted) {
            // 執行 Binding 操作
            executeBindings();
        }
        mIsExecutingPendingBindings = false;
    }
 
}

好的, 可以看到最終看到了它調用了 ViewDataBinding.executeBindings 執行 Binding 操作

三. View 與 ObservableFiled 的綁定

這個方法是交由子類重寫的, 我們探究一下 MainFragmentBindingImpl 中是如何實現的

public class MainFragmentBindingImpl extends MainFragmentBinding implements com.sharry.demo.mvvm.generated.callback.OnClickListener.Listener {
    
    
    @Override
    protected void executeBindings() {
        ......
        // 獲取 ViewModel, 因爲這個方法是 post 到 MessageQueue 中執行的
        // 此時 mViewModel 我們已經通過 setViewModel 賦值了
        com.sharry.demo.mvvm.ui.main.MainViewModel viewmodel = mViewmodel;
        // 這個便是 ViewModel.中的 messageText 的引用
        androidx.databinding.ObservableField<java.lang.String> viewmodelMessageText = null;
        java.lang.String viewmodelMessageTextGet = null;

        if ((dirtyFlags & 0xdL) != 0) {
                if (viewmodel != null) {
                    // 1. 獲取我們定義的 ObservableField 實例對象 
                    viewmodelMessageText = viewmodel.getMessageText();
                }
                // 2. 讓 本地屬性爲 0 的控制與這個 viewmodelMessageText 相互註冊
                updateRegistration(0, viewmodelMessageText);
                // 3. 獲取 ObservableField 中的值
                if (viewmodelMessageText != null) {
                    viewmodelMessageTextGet = viewmodelMessageText.get();
                }
        }
        ......
    }
    
}

通過這段代碼可以確定 View 與 Observable 的綁定操作是通過 updateRegistration 進行綁定的, 接下來我們看看具體的實現

public abstract class ViewDataBinding extends BaseObservable {
    
    private static final CreateWeakListener CREATE_PROPERTY_LISTENER = new CreateWeakListener() {
        @Override
        public WeakListener create(ViewDataBinding viewDataBinding, int localFieldId) {
            return new WeakPropertyListener(viewDataBinding, localFieldId).getListener();
        }
    };
    
    protected boolean updateRegistration(int localFieldId, Observable observable) {
        // 更新註冊器
        return updateRegistration(localFieldId, observable, CREATE_PROPERTY_LISTENER);
    }
 
    private WeakListener[] mLocalFieldObservers;
 
    private boolean updateRegistration(int localFieldId, Object observable,
            CreateWeakListener listenerCreator) {
        ......
        // 從控件觀察者表中獲取 localFieldId 對應的觀察者
        WeakListener listener = mLocalFieldObservers[localFieldId];
        // 首次進行註冊操作, 故爲 null
        if (listener == null) {
            //  調用 registerTo 進行後續操作
            registerTo(localFieldId, observable, listenerCreator);
            return true;
        }
        ......
        return true;
    }
    
    protected void registerTo(int localFieldId, Object observable,
            CreateWeakListener listenerCreator) {
        ......
        
        WeakListener listener = mLocalFieldObservers[localFieldId];
        if (listener == null) {
            // 1. 創建該控件對應的觀察者, 即 WeakPropertyListener.getListener() 
            listener = listenerCreator.create(this, localFieldId);
            // 緩存到觀察者表中
            mLocalFieldObservers[localFieldId] = listener;
            ......
        }
        // 2. 爲 這個觀察者 綁定要觀察的對象, 即 ViewModel 中我們定義的 ObservableFiled 屬性
        listener.setTarget(observable);
    }
    
}

好的, 可以看到這是一個典型的觀察者設計模式

  • ViewDataBinding 爲 View 創建了一個觀察者對象, 即 WeakListener, 用於監聽被觀察者
  • 調用 WeakListener.setTarget 綁定要觀察的對象, 即設置被觀察者

接下來我們看看這個 WeakListener 的創建過程, 以及它與 ObservableFiled 的綁定

一) 創建觀察者

public abstract class ViewDataBinding extends BaseObservable {
    
    private static class WeakPropertyListener extends Observable.OnPropertyChangedCallback
            implements ObservableReference<Observable> {
    
        final WeakListener<Observable> mListener;

        public WeakPropertyListener(ViewDataBinding binder, int localFieldId) {
            // 創建了 WeakListener
            mListener = new WeakListener<Observable>(binder, localFieldId, this);
        }
                
        @Override
        public WeakListener<Observable> getListener() {
            return mListener;
        }
        
        @Override
        public void addListener(Observable target) {
            // 調用了 Observable.addOnPropertyChangedCallback, 爲被觀察者訂閱一個觀察者
            target.addOnPropertyChangedCallback(this);
        }
    }
    
    private static class WeakListener<T> extends WeakReference<ViewDataBinding> {
        
        private final ObservableReference<T> mObservable;
        protected final int mLocalFieldId;
        private T mTarget;

        public WeakListener(ViewDataBinding binder, int localFieldId,
                ObservableReference<T> observable) {
            super(binder, sReferenceQueue);
            mLocalFieldId = localFieldId;
            // 即 WeakPropertyListener 對象
            mObservable = observable;
        }

        public void setTarget(T object) {
            ......
            // 獲取要觀察的對象
            mTarget = object;
            if (mTarget != null) {
                // 調用 WeakPropertyListener.addListener 
                mObservable.addListener(mTarget);
            }
        }
    }
}

好的, 可以觀察者 WeakListener 是由 WeakPropertyListener 在構造函數中實例化的

  • WeakListener.setTarget 方法會回調 WeakPropertyListener.addListener 爲被觀察者 Observable 訂閱觀察者

這裏可以看到雖然 ViewDataBinding 中維護的觀察者數組 mLocalFieldObservers 是 WeakListener, 但真正的觀察者爲 WeakPropertyListener, 接下來我們看看 Observable 訂閱觀察者的過程

二) 觀察者的訂閱與通知

我們知道我們在 ViewModel 中定義的 Observable 爲 ObservableFiled 類型, 其 addOnPropertyChangedCallback 實現爲 BaseObservable

public class BaseObservable implements Observable {
    private transient PropertyChangeRegistry mCallbacks;

    public BaseObservable() {
    }

    @Override
    public void addOnPropertyChangedCallback(@NonNull OnPropertyChangedCallback callback) {
        synchronized (this) {
            if (mCallbacks == null) {
                mCallbacks = new PropertyChangeRegistry();
            }
        }
        // 很簡單, mCallbacks 爲訂閱該被對象的觀察者集合
        mCallbacks.add(callback);
    }
    
}

可以看到 addOnPropertyChangedCallback 中的操作非常簡單, 其內部維護了一個 mCallbacks 對象, 維護了訂閱了該對象的觀察者

回顧

至此 DataBinding 框架便爲我們成功的將 View 與 ViewModel 中的數據綁定了, 他們的關係如下

四. ObservableField 通知觀察者數據更新

當被觀察者 Observable 調用 set 方法更新數據時, 便會通知訂閱其觀察者們, 我們看看它的實現

public class ObservableField<T> extends BaseObservableField implements Serializable {

    private T mValue;

    public void set(T value) {
        if (value != mValue) {
            mValue = value;
            // 通知觀察者數據變更了
            notifyChange();
        }
    }
    
}
public class BaseObservable implements Observable {
    
    public void notifyChange() {
        synchronized (this) {
            if (mCallbacks == null) {
                return;
            }
        }
        // 通知觀察者數據變更
        mCallbacks.notifyCallbacks(this, 0, null);
    }
    
}

可以看到 notifyChange 將任務分發給了 PropertyChangeRegistry.notifyCallbacks 我們看看它的實現

public class CallbackRegistry<C, T, A> implements Cloneable {
 
    private List<C> mCallbacks = new ArrayList<C>();   
 
    public synchronized void notifyCallbacks(T sender, int arg, A arg2) {
        ......
        notifyRecurse(sender, arg, arg2);
        ......
    }
   
    private void notifyRecurse(T sender, int arg, A arg2) {
        ......
        notifyCallbacks(sender, arg, arg2, startCallbackIndex, callbackCount, 0);
    }
    
    private final NotifierCallback<C, T, A> mNotifier;
    
    private void notifyCallbacks(T sender, int arg, A arg2, final int startIndex,
            final int endIndex, final long bits) {
        long bitMask = 1;
        // 遍歷訂閱的觀察者
        for (int i = startIndex; i < endIndex; i++) {
            if ((bits & bitMask) == 0) {
                // 通過 NotifierCallback 推送數據
                mNotifier.onNotifyCallback(mCallbacks.get(i), sender, arg, arg2);
            }
            bitMask <<= 1;
        }
    }

}

可看到推送數據的操作, 是由 NotifierCallback 完成的, 它是實例化是在 PropertyChangeRegistry 構造的時候進行的

public class PropertyChangeRegistry extends
        CallbackRegistry<Observable.OnPropertyChangedCallback, Observable, Void> {

    private static final CallbackRegistry.NotifierCallback<Observable.OnPropertyChangedCallback, Observable, Void> NOTIFIER_CALLBACK = new CallbackRegistry.NotifierCallback<Observable.OnPropertyChangedCallback, Observable, Void>() {
        @Override
        public void onNotifyCallback(Observable.OnPropertyChangedCallback callback, Observable sender,
                int arg, Void notUsed) {
            // 調用了 WeakPropertyListener.onPropertyChanged 推送數據
            callback.onPropertyChanged(sender, arg);
        }
    };

    public PropertyChangeRegistry() {
        super(NOTIFIER_CALLBACK);
    }
            
}

可以看到 NotifierCallback 的實現類爲 PropertyChangeRegistry.NOTIFIER_CALLBACK, 其內部調用了 WeakPropertyListener.onPropertyChanged 推送數據

public abstract class ViewDataBinding extends BaseObservable {
    
    private static class WeakPropertyListener extends Observable.OnPropertyChangedCallback
            implements ObservableReference<Observable> {
        
        @Override
        public void onPropertyChanged(Observable sender, int propertyId) {
            ViewDataBinding binder = mListener.getBinder();
            ......
            // 這裏調用了 ViewDataBinding.handleFieldChange 對 View 進行賦值操作
            binder.handleFieldChange(mListener.mLocalFieldId, sender, propertyId);
        }
                    
    }
    
    private void handleFieldChange(int mLocalFieldId, Object object, int fieldId) {
        ......
        // 1. 調用了子類的 onFieldChange
        boolean result = onFieldChange(mLocalFieldId, object, fieldId);
        // 2. 返回 true, 調用 requestRebind, 這會重新執行 MainFragmentBindingImpl.executeBindings
        if (result) {
            requestRebind();
        }
    }
    
}

public class MainFragmentBindingImpl extends MainFragmentBinding implements com.sharry.demo.mvvm.generated.callback.OnClickListener.Listener {

    @Override
    protected boolean onFieldChange(int localFieldId, Object object, int fieldId) {
        switch (localFieldId) {
            case 0 :
                return onChangeViewmodelMessageText((androidx.databinding.ObservableField<java.lang.String>) object, fieldId);
        }
        return false;
    }
    
    private boolean onChangeViewmodelMessageText(androidx.databinding.ObservableField<java.lang.String> ViewmodelMessageText, int fieldId) {
        if (fieldId == BR._all) {
            synchronized(this) {
                    mDirtyFlags |= 0x1L;
            }
            // 1.1 返回 true, 說明需要數據重置
            return true;
        }
        return false;
    }
    
    @Override
    protected void executeBindings() {
        .....
        // 真正的爲 View 賦值
        if ((dirtyFlags & 0xdL) != 0) {
            androidx.databinding.adapters.TextViewBindingAdapter.setText(this.btnCenter, viewmodelMessageTextGet);
        }
        ......
    }
    
}

好的, 可以看到觀察者收到數據推送後, 最終會重新調用 MainFragmentBindingImpl.executeBindings 爲控件更新數據, 至此一次數據推送就完成了

總結

通過上面的分析可知 DataBinding 的工作原理主要有如下三個重要的步驟

  • ViewDataBinding 的構建
    • 解析 xml 爲 view 視圖
    • 構建視圖對應的 ViewDataBinding
  • ViewDataBinidng 處理視圖與 ViewModel 中 Observable 的綁定
    • 爲 view 創建其對應的 WeakListener
    • WeakListener 的實現由 WeakPropertyListener 實現
    • 將 WeakPropertyListener 當做觀察者, 將其添加到 Observable 的訂閱集合中
  • 當 Observable 的數據變更時, 便會推送給其訂閱者集合
    • 調用 WeakPropertyListener.onPropertyChanged 處理數據變更
    • 通過 ID 找到 WeakPropertyListener 對應的 View
    • 通過 ViewDataBinding 的生成實現類的 executeBindings 更新指定 view 的數據

可以看到這是一個典型的觀察者的應用場景, TextView 爲觀察者, Observable 爲被觀察者, 數據變更時便會推送給觀察者更新數據, 當然這只是一個最基礎的應用場景, 其他的使用場景可以類比推理

問題的思考

爲什麼 <layout> 標籤, 會自動給 view 增加 tag

  • DataBinding 定義了 xml 解析器, 會給 view 增加 tag

爲什麼 要用 WeakListener 弱引用類型接口?

  • WeakListener 數組是被 ViewDataBinding 持有的, ViewDataBinding 在使用時會被 View 層持有
  • 當 WeakListener 被添加到數據源 ObservableFiled 中的訂閱集合中時, 會被 ViewModel 中的數據源屬性持有

我們知道 ViewModel 的移植性是非常強的, 因此若該 ViewModel 被另一個生命週期更長的 View 持有, 導致 ViewModel 的生命週期長於當前 View, 這會導致當前 View 無法釋放而造成內存泄漏

WeakListener 使用弱引用, 當內存即將溢出時, 會無視弱引用將該對象回收, 相當於做了一層保護措施, 但這並不意味着我們在開發過程中可以肆意妄爲, 代碼的質量還是要保證的, 內存泄漏更是需要體驗預判

DataBinding 設計上的優劣

  • 幫我們實現了 View 與 ViewModel 之間的交互, 讓使用者關心的更少, 可以更加專注於業務代碼的開發
  • 目前支持的屬性難以覆蓋常用應用場景, 編譯時生成類在一定程度上降低了編譯速度
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章