美文网首页
Android Jetpack Architecture原理之V

Android Jetpack Architecture原理之V

作者: ncmon | 来源:发表于2018-10-10 17:07 被阅读0次


      Jetpack已经出了很久很久了,近几年的GDD几乎每次都会介绍新的组件,说来惭愧,一直没有好好学习,看近年的Google 的很多Demo中其实都有所体现,之前都是大概的了解了一遍。最近决定,好好梳理一遍,既学习其用法,也尝试学习下其设计思想。也是时候该补充一下了。进入正题--ViewModel

      首先都是看官方的例子,ViewModel官方的的例子是会和另一个架构库LiveData写在一起,很多的博客也是照官方的例子来说明,开始接触时甚至给了我一种假象:ViewModel都是和LiveData一起使用的。后来阅读才了解,ViewModel和LiveData职责分工还是很明显的,使用LiveData Demo主要使用其observe功能,LiveDate的使用及原理之后再分析,甚至在appcompat-v7:27.1.1中直接单独集成了ViewModel.所以,故为排除干扰,今天不会使用官方的主流Demo用法,先来看ViewModel。

      Android的UI控制器(Activity和Fragment)从创建到销毁拥有自己完整的生命周期,当系统配置发生改变时((Configuration changes)),系统就会销毁Activity和与之关联的Fragment然后再次重建<font color=#FFA500>(可通过在AndroidManifast.xml中配置android:configChanges修改某些行为,这里不讨论)</font>,那么存储在当前UI中的临时数据也会被清空,例如,登陆输入框,输入账号或密码后旋转屏幕,视图被重建,输入过的数据也清空了,这无疑是一种不友好的用户体验。对于少量的可序列化数据可以使用onSaveInstanceState()方法保存然后在onCreate()方法中重新恢复,正如所说onSaveInstanceState对于大量的数据缓存有一定的局限性,大量的数据缓存则可以使用Fragment.setRetainInstance(true)来保存数据。ViewModel也是提供了相同的功能,其实和“RetainInstance”也有关联,用来存储和管理与UI相关的数据,允许数据在系统配置变化后存活,我们一起看一下这个ViewModel的缓存是怎么实现的呢?

    使用方式
    首先先看下使用方式,先上效果图

    ViewMode Sample
    public class MyViewModel extends ViewModel {
        String name;
        public String getName(){
            return name;
        }
        public void setName(String name){
            this.name = name;
        }
        @Override
        protected void onCleared() {
            super.onCleared();
            name = null;
        }
    }
    

     

    public class ViewModelActivity extends AppCompatActivity implements View.OnClickListener {
        private static final String TAG = "ViewModelActivity";
        TextView textView;
        private MyViewModel myViewModel;
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_viewmodel);
            textView = findViewById(R.id.textView);
            textView.setOnClickListener(this);
            ViewModelProvider.Factory factory = ViewModelProvider.AndroidViewModelFactory.getInstance(getApplication());
        /*
        *这里的this是ViewModelStoreOwner接口在appcompat-v7:27.1.1支持库中AppCompatActivity已经实现了,       
        *如果是较低版本,需要更新支持包或者参考其实现对本来继承的Activity做对应实现。
        */
            ViewModelProvider provider = new ViewModelProvider(this, factory);//
            myViewModel = provider.get(MyViewModel.class);
            Log.e(TAG, "onCreate: " + myViewModel.getName() );
            if (myViewModel.getName() != null) {
                textView.setText(myViewModel.getName());
            }
        }
        @Override
        public void onClick(View v) {
            switch (v.getId()){
                case R.id.textView:
                    myViewModel.setName("MyViewModel Test");
                    textView.setText(myViewModel.getName());
                    break;
            }
        }
    }
    

     

    <?xml version="1.0" encoding="utf-8"?>
    <android.support.constraint.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    
    
        <TextView
            android:id="@+id/textView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:layout_marginTop="8dp"
            android:layout_marginEnd="8dp"
            android:layout_marginBottom="8dp"
            android:text="default"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    
    </android.support.constraint.ConstraintLayout>
    

    非常简单的一个例子,这就是ViewModel最简单的使用了,就是TextView中显示ViewModel的数据。ViewModel需要由ViewModelProvider.get(Class<T>)来取得,旋转屏幕销毁后,之前改变的数据还在。
    发现的一些疑问
    接下来就是进入主题分析下ViewModel到底是怎么实现的呢?
    带着问题看源码:

    • ViewModelProvider是干啥的?
    • AndroidViewModelFactory 这命名一看就是应该是工厂模式,工厂创建了什么?
    • provider.get(MyViewModel.class) 这里直接使用的get命名就得到了需要的唯一数据
    • 注释中ViewModelStoreOwner又是什么角色?
      源码分析
      先看ViewModel类,没什么说的,就是一个么有任何真正实现的抽象类,只有一个抽象方法onCleared()
      public abstract class ViewModel {
          /**
           * This method will be called when this ViewModel is no longer used and will be destroyed.
           * <p>
           * It is useful when ViewModel observes some data and you need to clear this subscription to
           * prevent a leak of this ViewModel.
           */
          @SuppressWarnings("WeakerAccess")
          protected void onCleared() {
          }
      }
    

    接着看下ViewModelFactory,顾名思义就是制造ViewModel的。
    AndroidViewModelFactory的继承关系如下:

    android.arch.lifecycle.ViewModelProvider.Factory

    android.arch.lifecycle.ViewModelProvider.NewInstanceFactory

    android.arch.lifecycle.ViewModelProvider.AndroidViewModelFactory

    Factory是一个只包含一个create的interface,NewInstanceFactory实现了该方法传入Class<T>会利用ViewModel的默认无参构造器创建一个对应ViewModel的实例,而AndroidViewModelFactory增加了一个属性就是应用的Applicaion,同时重写create方法,查看ViewModel是否有包含Applicaion参数的构造方法从而使用,对应的其实还有一个AndroidViewModel是ViewModel的子类,默认已经实现了带有Application参数的构造方法,需要使用在ViewModel中使用application的直接继承AndroidViewModel就可以,看到这里其实最上面的例子有个不是问题的问题,其实上面的Factory直接使用NewInstanceFactory就可以创建出对应的ViewModel实例了。

        /**
         * Implementations of {@code Factory} interface are responsible to instantiate ViewModels.
         */
        public interface Factory {
            /**
             * Creates a new instance of the given {@code Class}.
             * <p>
             *
             * @param modelClass a {@code Class} whose instance is requested
             * @param <T>        The type parameter for the ViewModel.
             * @return a newly created ViewModel
             */
            @NonNull
            <T extends ViewModel> T create(@NonNull Class<T> modelClass);
        }
    
    
    /**
     * Simple factory, which calls empty constructor on the give class.
     */
    public static class NewInstanceFactory implements Factory {
    
        @SuppressWarnings("ClassNewInstance")
        @NonNull
        @Override
        public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
            //noinspection TryWithIdenticalCatches
            try {
                return modelClass.newInstance();
            } catch (InstantiationException e) {
                throw new RuntimeException("Cannot create an instance of " + modelClass, e);
            } catch (IllegalAccessException e) {
                throw new RuntimeException("Cannot create an instance of " + modelClass, e);
            }
        }
    }
    
    /**
     * {@link Factory} which may create {@link AndroidViewModel} and
     * {@link ViewModel}, which have an empty constructor.
     */
    public static class AndroidViewModelFactory extends ViewModelProvider.NewInstanceFactory {
    
        private static AndroidViewModelFactory sInstance;
    
        /**
         * Retrieve a singleton instance of AndroidViewModelFactory.
         *
         * @param application an application to pass in {@link AndroidViewModel}
         * @return A valid {@link AndroidViewModelFactory}
         */
        @NonNull
        public static AndroidViewModelFactory getInstance(@NonNull Application application) {
            if (sInstance == null) {
                sInstance = new AndroidViewModelFactory(application);
            }
            return sInstance;
        }
    
        private Application mApplication;
    
        /**
         * Creates a {@code AndroidViewModelFactory}
         *
         * @param application an application to pass in {@link AndroidViewModel}
         */
        public AndroidViewModelFactory(@NonNull Application application) {
            mApplication = application;
        }
    
        @NonNull
        @Override
        public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
            if (AndroidViewModel.class.isAssignableFrom(modelClass)) {
                //noinspection TryWithIdenticalCatches
                try {
                    return modelClass.getConstructor(Application.class).newInstance(mApplication);
                } catch (NoSuchMethodException e) {
                    throw new RuntimeException("Cannot create an instance of " + modelClass, e);
                } catch (IllegalAccessException e) {
                    throw new RuntimeException("Cannot create an instance of " + modelClass, e);
                } catch (InstantiationException e) {
                    throw new RuntimeException("Cannot create an instance of " + modelClass, e);
                } catch (InvocationTargetException e) {
                    throw new RuntimeException("Cannot create an instance of " + modelClass, e);
                }
            }
            return super.create(modelClass);
        }
    }
    

    之后通过ViewModelStoreOwner和刚刚创建的Factory创建出ViewModelPrivider实例

    /**
     * Creates {@code ViewModelProvider}, which will create {@code ViewModels} via the given
     * {@code Factory} and retain them in a store of the given {@code ViewModelStoreOwner}.
     *
     * @param owner   a {@code ViewModelStoreOwner} whose {@link ViewModelStore} will be used to
     *                retain {@code ViewModels}
     * @param factory a {@code Factory} which will be used to instantiate
     *                new {@code ViewModels}
     */
    public ViewModelProvider(@NonNull ViewModelStoreOwner owner, @NonNull Factory factory) {
        this(owner.getViewModelStore(), factory);
    }
    
    /**
     * Creates {@code ViewModelProvider}, which will create {@code ViewModels} via the given
     * {@code Factory} and retain them in the given {@code store}.
     *
     * @param store   {@code ViewModelStore} where ViewModels will be stored.
     * @param factory factory a {@code Factory} which will be used to instantiate
     *                new {@code ViewModels}
     */
    public ViewModelProvider(@NonNull ViewModelStore store, @NonNull Factory factory) {
        mFactory = factory;
        this.mViewModelStore = store;
    }
    
    /**
     * A scope that owns {@link ViewModelStore}.
     * <p>
     * A responsibility of an implementation of this interface is to retain owned ViewModelStore
     * during the configuration changes and call {@link ViewModelStore#clear()}, when this scope is
     * going to be destroyed.
     */
    @SuppressWarnings("WeakerAccess")
    public interface ViewModelStoreOwner {
        /**
         * Returns owned {@link ViewModelStore}
         *
         * @return a {@code ViewModelStore}
         */
        @NonNull
        ViewModelStore getViewModelStore();
    }
    

    ViewModelStoreOwner 也是一个接口是FragmentActivity实现了该接口并实现了其中的getViewModelStore()方法

    public class FragmentActivity extends BaseFragmentActivityApi16 implements
            ViewModelStoreOwner...{
        private ViewModelStore mViewModelStore;
        @Override
        protected void onCreate(@Nullable Bundle savedInstanceState) {
            mFragments.attachHost(null /*parent*/);
    
            super.onCreate(savedInstanceState);
    
            NonConfigurationInstances nc =
                    (NonConfigurationInstances) getLastNonConfigurationInstance();
            if (nc != null) {
                mViewModelStore = nc.viewModelStore;
        }
        /**
         * Returns the {@link ViewModelStore} associated with this activity
         *
         * @return a {@code ViewModelStore}
         */
        @NonNull
        @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) {
                mViewModelStore = new ViewModelStore();
            }
            return mViewModelStore;
        }
    }
    

    这个ViewModelStore又是什么呢,其实就是真正利用HashMap存储ViewModel的地方了,看下代码在存储和clear同时会调用ViewModel需要实现的抽象方法onClear()

    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);
        }
    
        /**
         *  Clears internal storage and notifies ViewModels that they are no longer used.
         */
        public final void clear() {
            for (ViewModel vm : mMap.values()) {
                vm.onCleared();
            }
            mMap.clear();
        }
    }
    

    这样ViewModelProvider就是有了一个ViewModel的容器,这时去调用ViewModelProvider的get(Class<T>)方法就是去调用mViewModelStore
    的get()方法取出对应的ViewModel所以这里只要持有的ViewModelStore是有缓存的,那么取出的ViewModel就是相同的缓存了。

    /**
     * Returns an existing ViewModel or creates a new one in the scope (usually, a fragment or
     * an activity), associated with this {@code ViewModelProvider}.
     * <p>
     * The created ViewModel is associated with the given scope and will be retained
     * as long as the scope is alive (e.g. if it is an activity, until it is
     * finished or process is killed).
     *
     * @param modelClass The class of the ViewModel to create an instance of it if it is not
     *                   present.
     * @param <T>        The type parameter for the ViewModel.
     * @return A ViewModel that is an instance of the given type {@code T}.
     */
    @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.
            }
        }
    
        viewModel = mFactory.create(modelClass);
        mViewModelStore.put(key, viewModel);
        //noinspection unchecked
        return (T) viewModel;
    }
    
    

    看到这里就会发现ViewModelStore的缓存其实是通过NonConfigurationInstances的缓存来实现的,这样就完成了Activity销毁重建后ViewModel还保存原来的数据的过程,那么NonConfigurationInstances 是什么呢?如果有了解过使用在Activity中使用onRetainNonConfigurationInstance()保存缓存数据,在onCreate()中通过getLastNonConfigurationInstance()恢复之前的数据状态的同学可能会很熟悉这里的写法,是的,这里FragmentActivity就是使用的这种方式来保存之前的ViewModelStore,看下FragmentActivity的onRetainNonConfigurationInstance()方法。

        /**
         * Retain all appropriate fragment state.  You can NOT
         * override this yourself!  Use {@link #onRetainCustomNonConfigurationInstance()}
         * if you want to retain your own state.
         */
        @Override
        public final Object onRetainNonConfigurationInstance() {
            if (mStopped) {
                doReallyStop(true);
            }
    
            Object custom = onRetainCustomNonConfigurationInstance();
    
            FragmentManagerNonConfig fragments = mFragments.retainNestedNonConfig();
    
            if (fragments == null && mViewModelStore == null && custom == null) {
                return null;
            }
    
            NonConfigurationInstances nci = new NonConfigurationInstances();
            nci.custom = custom;
            nci.viewModelStore = mViewModelStore;//就是这里了,会把之前的VeiwmodelStroe存储到NonConfigurationInstances中以供后续恢复使用
            nci.fragments = fragments;
            return nci;
        }
    

    这里其实再次出现了一个问题 onRetainNonConfigurationInstance()和getLastNonConfigurationInstance()又是怎么恢复数据呢?...这个其实和Activity的启动流程相关,这里也介绍一下吧,之后的内容其实是Activity的内容了,趁这次看ViwModel也跟着看了一遍,有了解过Activity启动流程的同学更容易理解的多,大家酌情观看。

    也不能从头开始说起,再从头就要越扯越远了,就从ActivityThread.java中的scheduleLaunchActivity开始

            @Override
            public final void scheduleLaunchActivity(Intent intent, IBinder token, int ident,
                    ActivityInfo info, Configuration curConfig, Configuration overrideConfig,
                    CompatibilityInfo compatInfo, String referrer, IVoiceInteractor voiceInteractor,
                    int procState, Bundle state, PersistableBundle persistentState,
                    List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents,
                    boolean notResumed, boolean isForward, ProfilerInfo profilerInfo) {
    
                updateProcessState(procState, false);
    
                ActivityClientRecord r = new ActivityClientRecord();
    
                r.token = token;
                r.ident = ident;
                r.intent = intent;
                r.referrer = referrer;
                r.voiceInteractor = voiceInteractor;
                r.activityInfo = info;
                r.compatInfo = compatInfo;
                r.state = state;
                r.persistentState = persistentState;
    
                r.pendingResults = pendingResults;
                r.pendingIntents = pendingNewIntents;
    
                r.startsNotResumed = notResumed;
                r.isForward = isForward;
    
                r.profilerInfo = profilerInfo;
    
                r.overrideConfig = overrideConfig;
                updatePendingConfiguration(curConfig);
    
                sendMessage(H.LAUNCH_ACTIVITY, r);
            }
    

    从ActivityThread.java中H(extents Handler)接收到LAUNCH_ACTIVITY,并且会接收ActivityClientRecord,其中会调用ActivityThread的handleLaunchActivity方法:

        //ActivityThread.java
        //没有前后文的H中的handleMessage~~~
            public void handleMessage(Message msg) {
                if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
                switch (msg.what) {
                    case LAUNCH_ACTIVITY: {
                        Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
                        final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
                //ActivityClientRecord 是apk进程中一个Activity的代表,这个对象的activity成员引用真正的Activity组件,后面的都和它有关系
                        r.packageInfo = getPackageInfoNoCheck(
                                r.activityInfo.applicationInfo, r.compatInfo);
                        handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");///这里~这里~
                        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
                    } break;
    
        private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent, String reason) {
                ...
                Activity a = performLaunchActivity(r, customIntent);
            ...
        }
    
        private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
        ...
            if (activity != null) {
                    CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
                    Configuration config = new Configuration(mCompatConfiguration);
                    if (r.overrideConfig != null) {
                        config.updateFrom(r.overrideConfig);
                    }
                    if (DEBUG_CONFIGURATION) Slog.v(TAG, "Launching activity "
                            + r.activityInfo.name + " with config " + config);
                    Window window = null;
                    if (r.mPendingRemoveWindow != null && r.mPreserveWindow) {
                        window = r.mPendingRemoveWindow;
                        r.mPendingRemoveWindow = null;
                        r.mPendingRemoveWindowManager = null;
                    }
                    appContext.setOuterContext(activity);
                    activity.attach(appContext, this, getInstrumentation(), r.token,
                            r.ident, app, r.intent, r.activityInfo, title, r.parent,
                            r.embeddedID, r.lastNonConfigurationInstances, config, //看到这个r.lastNonConfigurationInstances 就是在Activity方法中调用getLastNonConfigurationInstance()获取到的Object了。
                            r.referrer, r.voiceInteractor, window, r.configCallback);
        ...
        }
    

    注释中的地方就是lastNonConfigurationInstances的赋值的地方,可能会发现在scheduleLaunchActivity并没有对lastNonConfigurationInstances赋值,因为第一次启动Activity时,这里其实就是null的,那么赋值的地方在哪里呢,既然是销毁后会恢复数据,追踪发现在performDestroyActivity()也就是在调用onDestroy生命周期之前有这样一段代码

    private ActivityClientRecord performDestroyActivity(IBinder token, boolean finishing,
                int configChanges, boolean getNonConfigInstance) {
            ActivityClientRecord r = mActivities.get(token);
            ...无关代码省略
                if (getNonConfigInstance) {
                    try {
                        r.lastNonConfigurationInstances
                                = r.activity.retainNonConfigurationInstances();///就是这里出现了想要找的NonConfigurationInstances
                    } catch (Exception e) {
                        if (!mInstrumentation.onException(r.activity, e)) {
                            throw new RuntimeException(
                                    "Unable to retain activity "
                                    + r.intent.getComponent().toShortString()
                                    + ": " + e.toString(), e);
                        }
                    }
                }
                try {
                    r.activity.mCalled = false;
                    mInstrumentation.callActivityOnDestroy(r.activity);
        ...无关代码省略
            return r;
        }
    

    在performDestroyActivity()调用了Activity.retainNonConfigurationInstances()方法了,所以逻辑切换回Activity中...

         /**
         * This method is similar to {@link #onRetainNonConfigurationInstance()} except that
         * it should return either a mapping from  child activity id strings to arbitrary objects,
         * or null.  This method is intended to be used by Activity framework subclasses that control a
         * set of child activities, such as ActivityGroup.  The same guarantees and restrictions apply
         * as for {@link #onRetainNonConfigurationInstance()}.  The default implementation returns null.
         */
        @Nullable
        HashMap<String,Object> onRetainNonConfigurationChildInstances() {
            return null;
        }
    
        NonConfigurationInstances retainNonConfigurationInstances() {
            Object activity = onRetainNonConfigurationInstance();///熟悉的代码,原来的配方,和分析ActivityThread之前联系起来了,在Activity中是空实现,这里就是获取子类的NonConfigurationInstance(),之前的例子就是的得FragmentActivity中的具体实现,上文中已经在分析ActivityThread.java已经指出。
            HashMap<String, Object> children = onRetainNonConfigurationChildInstances();
            FragmentManagerNonConfig fragments = mFragments.retainNestedNonConfig();
    
            // We're already stopped but we've been asked to retain.
            // Our fragments are taken care of but we need to mark the loaders for retention.
            // In order to do this correctly we need to restart the loaders first before
            // handing them off to the next activity.
            mFragments.doLoaderStart();
            mFragments.doLoaderStop(true);
            ArrayMap<String, LoaderManager> loaders = mFragments.retainLoaderNonConfig();
    
            if (activity == null && children == null && fragments == null && loaders == null
                    && mVoiceInteractor == null) {
                return null;
            }
    
            NonConfigurationInstances nci = new NonConfigurationInstances();
            nci.activity = activity;
            nci.children = children;
            nci.fragments = fragments;
            nci.loaders = loaders;
            if (mVoiceInteractor != null) {
                mVoiceInteractor.retainInstance();
                nci.voiceInteractor = mVoiceInteractor;
            }
            return nci;//这里返回的是Activity中的NonConfigurationInstances就保存在了ActivityClientRecord中了
        }
    

    至此,ActivityClientRecord就不再深入了,可以看到在Activity中是以一个ArrayMap来保存Activity的记录,记录的就是Activity的状态,所以这里就实现了对NonConfigurationInstances的保存。


    结语:至此就基本看完了ViewModel在Activity中的使用和原理,在Fragment中的实现主要是使用setRetainInstance(true)的方式去保存,跟今天的分析也有关联,分析源码的过程总是看着就有新的问题,再次带着问题去解决会再次有不同的收获,本文的理解也可能有偏差,如有错误和想要交流的也欢迎指正沟通。

    相关文章

      网友评论

          本文标题:Android Jetpack Architecture原理之V

          本文链接:https://www.haomeiwen.com/subject/idcmaftx.html