zoukankan      html  css  js  c++  java
  • Android -- APT手写实现ARouter功能

    1,随着需求越来越多,项目也越来越大,实现项目的组件化便成为了迫切需要解决的技术点,随着去年一个多月的重构,我们最后使用了cc来实现了项目的组件化,今天咋们先不来讲cc,来和大家一起看看阿里的ARouter是怎么实现的。

    2,对比传统项目我们基本是把所有的业务逻辑放在app的module里面,如果同一个业务涉及到其它模块业务的话就需要看其它模块的业务逻辑,这样对我们后期的迭代和维护有着比较高的成本,且当项目大的时候整个项目一起编译的话需要很久的时间(我们项目重构前编译一次要十多分钟,重构之后只需要两分多钟),当我们实现了组件化,就会把项目按照业务模块来拆分多个module,例如:登录模块、订单模块、消息模块、历史记录模块等等,这样每个模块负责人只用关心自己负责的模块,再把自己这个模块需要对外暴露的能力给暴露出去,如果在开发调试自己模块的时候,使用组件化后可以直接编译自己模块的module,这样可以缩短项目的编译时间

       怎么实现一个module从可独立运行的app到lib的自由切换呢?很简单其实就是我们每个module下的build.gradle里面的

    apply plugin: 'com.android.library' //lib
    apply plugin: 'com.android.application'//app 

      我们来简单的写一下,先来创建一个项目,里面有一个默认的app的module ,正常情况下我们项目里面需要实现一个登陆功能,这时候我创建一个login的Android lib,在lib里面来实现我们基本的登录界面和功能,那这时候因为到实现我们组件化的基本功能,每个模块能够独立运行和调试,我们在开发的阶段就需要把login这个Android lib转换为一个可独立运行的applicaiton ,其实很简单 ,只需要在项目目录下的gradle.properties定义一个变量LOGIN_IS_LIB,用来表示是否将login这个module当做lib来运行

       然后再在我们login的module下面的build.gradle 加一些逻辑判断

    //① 判断是将次module编译成lib还是application
    if (LOGIN_IS_LIB.toBoolean()){
        apply plugin: 'com.android.library'
    }else {
        apply plugin: 'com.android.application'
    }
    
    android {
        compileSdkVersion 29
        buildToolsVersion "29.0.2"
    
        defaultConfig {
            minSdkVersion 19
            targetSdkVersion 29
            versionCode 1
            versionName "1.0"
    
            testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
            consumerProguardFiles 'consumer-rules.pro'
        }
    
        buildTypes {
            release {
                minifyEnabled false
                proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            }
        }
        //② 添加清单文件判断 
        sourceSets{
            main{
                if (LOGIN_IS_LIB.toBoolean()){
                    manifest.srcFile 'src/main/lib_manifest/AndroidManifest.xml'
                }else {
                    manifest.srcFile 'src/main/AndroidManifest.xml'
                }
            }
        }
    }
    
    dependencies {
        implementation fileTree(dir: 'libs', include: ['*.jar'])
    
        implementation 'androidx.appcompat:appcompat:1.1.0'
        implementation 'com.google.android.material:material:1.0.0'
        implementation 'androidx.annotation:annotation:1.1.0'
        implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
        implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
        testImplementation 'junit:junit:4.12'
        androidTestImplementation 'androidx.test.ext:junit:1.1.1'
        androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
        implementation project(path: ':arouter')
        implementation project(path: ':annotation')
        annotationProcessor project(path: ':process')
    }
    

      很简单  通过控制gradle.properties定义一个变量LOGIN_IS_LIB的值就可以来解决这个问题了 ,对了顺便提一下,因为我们app是主module且依赖于login的module,所以我们在app的build.gradle文件也要添加一下判断

     if (LOGIN_IS_LIB.toBoolean()){
            implementation project(path: ':login')
    }
    

      好了,这就是我们简单的实现组件化的一下部分,实现一个module在lib和application的切换,接下来进入到我们的正题

    3,在我们传统的代码来实现页面的跳转基本上是使用startActivity来跳转的,那么我们就会写出一下代码

    Intent intent = new Intent() ;
    intent.setClass(mContext ,aCls) ;
    mContext.startActivity(intent);
    

      如果我们想对这跳转页面的功能稍微的封装一下,那么我们就建立一个Arouter类,里面提供一个jumpActivity的方法,jumpActivity里面除了我们的cls参数,再添加一个bundle参数用来传递参数

    package com.ysten.fuxi01;
    
    import android.app.Activity;
    import android.content.Context;
    import android.content.Intent;
    import android.os.Bundle;
    import android.text.TextUtils;
    import android.util.ArrayMap;
    import android.util.Log;
    
    import com.ysten.arouter.IRouter;
    
    import java.util.ArrayList;
    import java.util.Enumeration;
    import java.util.List;
    import java.util.Map;
    
    import dalvik.system.DexFile;
    
    /**
     * @author wangjitao on 2020/5/5
     * @desc:
     */
    public class Arouter {
    
        private static Arouter instance ;
        private  Context mContext;
    
        public static Arouter getInstance(){
            if (instance == null){
                synchronized (Arouter.class){
                    instance = new Arouter() ;
                }
    
            }
            return instance ;
        }
        
        public void jumpActivity(Class cls){
            jumpActivity(cls,null);
        }
    
        public void jumpActivity(Class cls, Bundle bundle){
            Intent intent = new Intent() ;
            if (bundle != null){
                intent.putExtras(bundle);
            }
            intent.setClass(mContext ,cls) ;
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            mContext.startActivity(intent);
        }
    }
    

      上面的代码很简单 ,大家应该没有什么疑问的,在多人开发同一个项目的时候,很多时候我们都是需要去打开别人的界面的,这时候我们需要知道别人的这个类的class名字,直接持有这个class$,从而造成了强依赖关系,提高了耦合度,这个时候用到阿里的Arouter的同学会知道,阿里的Arouter通过注解的方式,将每一个每一个Activity绑定一个path,从而对接人员只需要知道path,就可以跳转到对应的Activity了

    @Route(path = "/app/MainActivity")
    public class MainActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            findViewById(R.id.btn_test).setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    ARouter.getInstance().build("/app/MainActivity1").navigation() ;
                    //startActivity(new Intent(MainActivity.this, LoginActivity.class));
                }
            });
        }
    }
    

      那么我们按照阿里的这种思路我们继续来完善我们的代码,这时候我们创建一个map,将我们的activity和我们的path进行绑定,然后每次跳转的时候,只需将制定的path传入就行

    package com.ysten.fuxi01;
    
    import android.app.Activity;
    import android.content.Context;
    import android.content.Intent;
    import android.os.Bundle;
    import android.text.TextUtils;
    import android.util.ArrayMap;
    import android.util.Log;
    
    import java.util.Map;
    
    /**
     * @author wangjitao on 2020/5/5
     * @desc:
     */
    public class Arouter {
    
        private static Arouter instance ;
        private  Context mContext;
        private static Map<String,Class<? extends Activity>> activityMap ;
    
        public static Arouter getInstance(){
            if (instance == null){
                synchronized (Arouter.class){
                    instance = new Arouter() ;
                    activityMap = new ArrayMap<>();
                }
    
            }
            return instance ;
        }
    
        /**
         *  将activity压入
         * @param activityName
         * @param cls
         */
        public void putActivity(String activityName ,Class cls){
            if (cls != null && !TextUtils.isEmpty(activityName)){
                activityMap.put(activityName,cls) ;
            }
        }
    
        /**
         * 通过之前定义的path就行启动
         * @param activityName
         */
        public void jumpActivity(String activityName){
            jumpActivity(activityName,null);
        }
    
        public void jumpActivity(String activityName, Bundle bundle){
            Intent intent = new Intent() ;
            Class<? extends Activity> aCls = activityMap.get(activityName);
            if (aCls == null){
                Log.e("wangjitao" ," error -- > can not find activityName "+activityName);
                return;
            }
            if (bundle != null){
                intent.putExtras(bundle);
            }
            intent.setClass(mContext ,aCls) ;
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            mContext.startActivity(intent);
        }
    }
    

       这时候我们就只需要调用Arouter.getInstance().jumpActivity("/login/LoginActivity");来进行跳转了  ,但是这样的前提是我们在初始化的时候需要把我们的所有的activity给put到map集合中,即调用Arouter.getInstance().putActivity("/com/MainActivity",com.ysten.fuxi01.MainActivity.class); 我们先来谢谢这种代码,先创建接口IRouter

    package com.ysten.arouter;
    
    import android.app.Activity;
    
    /**
     * @author wangjitao on 2020/5/5
     * @desc:
     */
    public interface IRouter {
        void putActivity() ;
    }
    

      再创建我们的实现类ActivityUtils,在putActivity()方法中我们需要将本module下的所有Activity给压入

    package com.ysten.fuxi01;
    
    import com.ysten.arouter.Arouter;
    import com.ysten.arouter.IRouter;
    
    public class ActivityUtils implements IRouter {
        @Override
        public void putActivity() {
            //现在手动的添加Activity堆栈管理
            Arouter.getInstance().putActivity("/com/MainActivity",com.ysten.fuxi01.MainActivity.class);
        }
    }
    

      存在多个module的话  ,上面这和个类需要被复制粘贴多次,这样就有点得不偿失了,所以我们打算apt和注解技术,在编译的时候扫描我们指定的注解,然后输出成对应的文件,从而解决创建重复类和写重复代码的问题,简单来说就是JVM会在编译期就运行APT去扫描处理代码中的注解然后输出java文件,ok,知道了这个思路我们就开干,先创建一个annotation的java lib,主要作用就是放置我们的注解文件,我们之前看到阿里的ARouter是使用Route注解的,所以我们这里也创建一个Route注解,添加path参数,注解之前我讲过了就不和大家解释里面的东西了,是一个最基本的注解

    package com.ysten.annotation;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    /**
     * @author wangjitao on 2020/5/5
     * @desc:
     */
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.CLASS)
    public @interface Route {
        String path();
    }
    

      ok,然后我们在创建一个名为process的java lib  ,这个lib主要是我们过滤注解,然后为每个module创建ActiivityUtils文件的地方,这是我们APT(Annotation Processing Tool)的核心内容,在process的module中我们先来引入几个库

    apply plugin: 'java-library'
    
    dependencies {
        implementation fileTree(dir: 'libs', include: ['*.jar'])
        implementation project(path: ':annotation')
        implementation 'com.google.auto.service:auto-service:1.0-rc6'
        implementation 'com.squareup:javapoet:1.12.1'
        annotationProcessor 'com.google.auto.service:auto-service:1.0-rc6'
    }
    
    sourceCompatibility = "7"
    targetCompatibility = "7"
    

      然后自定义processor ,创建Process类,集成自AbstractProcessor,且添加注解@AutoService(Processor.class) 

    @AutoService(Processor.class)
    public class Process extends AbstractProcessor {
        Filer filer ; //
    
        @Override
        public synchronized void init(ProcessingEnvironment processingEnv) {
            super.init(processingEnv);
            filer =  processingEnv.getFiler() ;
        }
    
        @Override
        public SourceVersion getSupportedSourceVersion() {
            return processingEnv.getSourceVersion();
        }
    
        /**
         * @return
         */
        @Override
        public Set<String> getSupportedAnnotationTypes() {
            Set<String> types = new HashSet<>() ;
            types.add(Route.class.getCanonicalName()) ;
            return types;
        }
    
        @Override
        public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        // todo 创建ActivityUtils文件  
        return false ;
    }
    }
    

      因为我们要为每个module生成ActivituUtils类,所以要是用到文件创建这块,需要一个Filer变量,然后我再来说一下这四个方法

    init:初始化。可以得到ProcessingEnviroment,ProcessingEnviroment提供很多有用的工具类Elements, Types 和 Filer
    getSupportedAnnotationTypes:指定这个注解处理器是注册给哪个注解的,这里说明是注解Route
    getSupportedSourceVersion:指定使用的Java版本,通常这里返回SourceVersion.latestSupported() 默认写法
    process:可以在这里写扫描、处理注解的代码,生成Java文件(process中的代码下面详细说明)
    

      现在就到了最关键的点了,我们首先通过RoundEnvironment获取到当前被@Route修饰的类,再取出Route的path和被修饰的Activity名称

     Set<? extends Element> elementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(Route.class);
            Map<String ,String> map = new HashMap<>() ;
            for (Element element :elementsAnnotatedWith ){
                // TypeElement
                // VariableElement
                TypeElement typeElement = (TypeElement)element ;
                //com.ysten.fuxi01.MainActivity
                String className = typeElement.getQualifiedName().toString();
                String pathName = typeElement.getAnnotation(Route.class).path();
                map.put(pathName,className+".class");
            }
    

      虽然里面的代码比较陌生 ,但是都是一些api方法,按照这个写就行,这时候 我们的map中就存放了关于我们以path内容为key、activity的class为value的数据了,现在就是创建ActivityUtils类,再将这行Arouter.getInstance().putActivity("/com/MainActivity",com.ysten.fuxi01.MainActivity.class);代码写入到文件中

     Writer writer = null ;
    String className = "ActivityUtils"+System.currentTimeMillis();
    JavaFileObject classFile = filer.createSourceFile("com.ysten.test." + className);
    

      这里我们通过filer.createSourceFile() 方法吧文件创建出来,前面是包名随便写,后面是className,就是我们的ActivityUtils,这里要注意因为存在多个module,都会创建ActivityUtils类且都在包com.ysten.test的包下,这里为了防止文件重复,我直接在后面加上了时间戳来区别,接下来就是我们写入文件内容的东西了

    writer = classFile.openWriter() ;
                writer.write("package com.ysten.test;
    " +
                        "
    " +
                        "import com.ysten.arouter.Arouter;
    " +
                        "import com.ysten.arouter.IRouter;
    " +
                        "
    " +
                        "public class "+className+" implements IRouter {
    " +
                        "    @Override
    " +
                        "    public void putActivity() {
    ");
    
                Iterator<String> iterator = map.keySet().iterator();
                while (iterator.hasNext()){
                    String activityKey = iterator.next() ;
                    String cls = map.get(activityKey);
                    writer.write("        Arouter.getInstance().putActivity(");
                    writer.write("""+activityKey+"","+cls+");");
                }
                writer.write("
    }
    " +
                        "}");
    

      没什么好说的,就是需要细心,我写这个的时候写错了一个文件名,找了一个多小时的问题,所以这个大家要细心  ,都是一些api方法的调用,约定俗成的,我把整个文件放上来

    package com.ysten.process;
    
    import com.google.auto.service.AutoService;
    import com.ysten.annotation.Route;
    
    import java.io.IOException;
    import java.io.Writer;
    import java.util.HashMap;
    import java.util.HashSet;
    import java.util.Iterator;
    import java.util.Map;
    import java.util.Set;
    
    import javax.annotation.processing.AbstractProcessor;
    import javax.annotation.processing.Filer;
    import javax.annotation.processing.ProcessingEnvironment;
    import javax.annotation.processing.Processor;
    import javax.annotation.processing.RoundEnvironment;
    import javax.lang.model.SourceVersion;
    import javax.lang.model.element.Element;
    import javax.lang.model.element.TypeElement;
    import javax.tools.JavaFileObject;
    
    /**
     * @author wangjitao on 2020/5/5
     * @desc:
     */
    @AutoService(Processor.class)
    public class Process extends AbstractProcessor {
        Filer filer ; //
    
        @Override
        public synchronized void init(ProcessingEnvironment processingEnv) {
            super.init(processingEnv);
            filer =  processingEnv.getFiler() ;
        }
    
        @Override
        public SourceVersion getSupportedSourceVersion() {
            return processingEnv.getSourceVersion();
        }
    
        /**
         * @return
         */
        @Override
        public Set<String> getSupportedAnnotationTypes() {
            Set<String> types = new HashSet<>() ;
            types.add(Route.class.getCanonicalName()) ;
            return types;
        }
    
        @Override
        public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
            //生成文件代码
            Set<? extends Element> elementsAnnotatedWith = roundEnv.getElementsAnnotatedWith(Route.class);
            Map<String ,String> map = new HashMap<>() ;
            for (Element element :elementsAnnotatedWith ){
                // TypeElement
                // VariableElement
                TypeElement typeElement = (TypeElement)element ;
                //com.ysten.fuxi01.MainActivity
                String className = typeElement.getQualifiedName().toString();
                String pathName = typeElement.getAnnotation(Route.class).path();
                map.put(pathName,className+".class");
            }
            if (map.size() == 0 ){
                return false;
            }
    
            //
            Writer writer = null ;
            //
            String className = "ActivityUtils"+System.currentTimeMillis();
            try {
                JavaFileObject classFile = filer.createSourceFile("com.ysten.test." + className);
                writer = classFile.openWriter() ;
                writer.write("package com.ysten.test;
    " +
                        "
    " +
                        "import com.ysten.arouter.Arouter;
    " +
                        "import com.ysten.arouter.IRouter;
    " +
                        "
    " +
                        "public class "+className+" implements IRouter {
    " +
                        "    @Override
    " +
                        "    public void putActivity() {
    ");
    
                Iterator<String> iterator = map.keySet().iterator();
                while (iterator.hasNext()){
                    String activityKey = iterator.next() ;
                    String cls = map.get(activityKey);
                    writer.write("        Arouter.getInstance().putActivity(");
                    writer.write("""+activityKey+"","+cls+");");
                }
                writer.write("
    }
    " +
                        "}");
            } catch (Exception e) {
                e.printStackTrace();
            }finally {
                if (writer != null){
                    try {
                        writer.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
    
            return false;
        }
    }
    

      这时候我们可以编译项目,可以在build/generated/ap_generated_sources/debug/out目录下来看到我们生成的文件

       ok,现在我们在每一个module下都生成了ActivityUtils文件,接下来就是找到所有的ActivityUtils文件,调用它的putActivity方法,那么我们找到com.ysten.test下的所有文件,代码如下 很简单

    public List<String> getAllActivityUtils(String packageName){
            List<String> list = new ArrayList<>() ;
            String path  ;
            try {
                path = mContext.getPackageManager().getApplicationInfo(mContext.getPackageName(), 0).sourceDir;
                DexFile dexFile = null ;
                dexFile = new DexFile(path);
                Enumeration enumeration = dexFile.entries() ;
                while(enumeration.hasMoreElements()){
                    String name = (String) enumeration.nextElement();
                    if (name.contains(packageName)){
                        list.add(name) ;
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            return list ;
        }
    

      找到这些文件之后就是需要生成对象,调用它的put方法了,我们第一反应就是反射,也很简单,知道了类名  ,直接执行方法

     List<String> className =getAllActivityUtils("com.ysten.test");
            Log.d("wangjitao" ,"className "+className);
            for (String cls : className ) {
                try {
                    Class<?> aClass =  Class.forName(cls);
                    if (IRouter.class.isAssignableFrom(aClass)){
                        IRouter iRouter = (IRouter) aClass.newInstance();
                        iRouter.putActivity();
                    }
                }catch (Exception e ){
    
                }
    
            }
    

      ok,到这里我们基本上把功能完成的差不多了,这里我们再在Arouter类里面添加初始化方法,在application的oncreate方法进行初始化,Arouter的完整代码如下

    package com.ysten.arouter;
    
    import android.app.Activity;
    import android.content.Context;
    import android.content.Intent;
    import android.os.Bundle;
    import android.text.TextUtils;
    import android.util.ArrayMap;
    import android.util.Log;
    
    import java.util.ArrayList;
    import java.util.Enumeration;
    import java.util.List;
    import java.util.Map;
    
    import dalvik.system.DexFile;
    
    /**
     * @author wangjitao on 2020/5/5
     * @desc:
     */
    public class Arouter {
    
        private static Arouter instance ;
        private  Context mContext;
        private static Map<String,Class<? extends Activity>> activityMap ;
    
        public static Arouter getInstance(){
            if (instance == null){
                synchronized (Arouter.class){
                    instance = new Arouter() ;
                    activityMap = new ArrayMap<>();
                }
    
            }
            return instance ;
        }
    
        public void putActivity(String activityName ,Class cls){
            if (cls != null && !TextUtils.isEmpty(activityName)){
                activityMap.put(activityName,cls) ;
            }
        }
    
        public void init(Context context){
            mContext = context ;
            List<String> className =getAllActivityUtils("com.ysten.test");
            Log.d("wangjitao" ,"className "+className);
            for (String cls : className ) {
                try {
                    Class<?> aClass =  Class.forName(cls);
                    if (IRouter.class.isAssignableFrom(aClass)){
                        IRouter iRouter = (IRouter) aClass.newInstance();
                        iRouter.putActivity();
                    }
                }catch (Exception e ){
    
                }
    
            }
        }
    
        public void jumpActivity(String activityName){
            jumpActivity(activityName,null);
        }
    
        public void jumpActivity(String activityName, Bundle bundle){
            Intent intent = new Intent() ;
            Class<? extends Activity> aCls = activityMap.get(activityName);
            if (aCls == null){
                Log.e("wangjitao" ," error -- > can not find activityName "+activityName);
                return;
            }
            if (bundle != null){
                intent.putExtras(bundle);
            }
            intent.setClass(mContext ,aCls) ;
            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            mContext.startActivity(intent);
        }
    
        public List<String> getAllActivityUtils(String packageName){
            List<String> list = new ArrayList<>() ;
            String path  ;
            try {
                path = mContext.getPackageManager().getApplicationInfo(mContext.getPackageName(), 0).sourceDir;
                DexFile dexFile = null ;
                dexFile = new DexFile(path);
                Enumeration enumeration = dexFile.entries() ;
                while(enumeration.hasMoreElements()){
                    String name = (String) enumeration.nextElement();
                    if (name.contains(packageName)){
                        list.add(name) ;
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            return list ;
        }
    
    }
    

      MainApplication.java

    public class MyApplication extends Application {
        @Override
        public void onCreate() {
            super.onCreate();
            initARouter();
        }
        private void initARouter(){
            Arouter.getInstance().init(this);
    }
    

      两个Activity代码如下

    @Route(path = "/app/MainActivity")
    public class MainActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            findViewById(R.id.btn_test).setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    Arouter.getInstance().jumpActivity("/login/LoginActivity");
                }
            });
        }
    }
    
    @Route(path = "/login/LoginActivity")
    public class LoginActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
        }
    }
    

      最后再看看效果

      ok,上面就是今天的所有内容,我们来总结总结知识点:

      ① Android项目的组件化实现,快速切换application或lib

      ②使用APT技术,编译时自动生成文件

      ③获取指定包名下所有的文件

      ④通过反射实现类的创建和方法的调用

       github项目链接

      最后,期待和大家再次相见

  • 相关阅读:
    Django -- 路由系统(URLconf)
    Django简介
    jQuery
    DOM
    JavaScript
    HTML,CSS
    Redis PK Memcached
    ORM框架-SQLAlchemy
    Memcached操作以及用法
    Py3快速下载地址
  • 原文地址:https://www.cnblogs.com/wjtaigwh/p/12839116.html
Copyright © 2011-2022 走看看