美文网首页Android收藏集组件化Android开发
Android组件化工程结构以及项目具体实施方案

Android组件化工程结构以及项目具体实施方案

作者: Joker_Wan | 来源:发表于2019-12-09 13:34 被阅读0次

    组件化优点

    1、代码解耦

    2、方便多人协作开发

    3、可复用性高,不同的APP可复用不同组件,提高开发效率

    4、每个组件可独立运行,减少编译时间,方便开发调试

    组件化工程结构

    Android组件化架构.png

    第一层:空壳app。应用的入口,可存放启动页,依赖所有业务组件

    第二层:业务组件。根据不同业务横向拆分出来的业务组件。任何一个业务组件都可以独立出来成为一个应用

    第三层:功能组件。通用业务是从应用业务中抽取出来的交集,从应用上说,他属于业务,而针对应用业务而言则更像是一种功能,好比登录这种业务功能,不需要关心有没有界面,当中是怎样的逻辑,只需要提供结果即可

    第四层:公共业务组件和公共服务组件公共业务组件:业务相关Base代码。公共服务组件:可存放各个组件对外暴露的接口,接口实现在组件内部,可通过ARouter或者DI(依赖注入)实现跨组件服务调用;可存放路由跳转等信息和路由服务。

    第五层:业务无关基础组件。网络请求、图片加载、存储、utils、通用View的封装

    项目组件化

    1、代码解耦

    代码解耦主要是从两个方面,其一是公共代码的抽取和归纳,其二是面向接口编程,接口下沉。

    公共代码的抽取和归纳:

    部分通用的功能性的代码抽出成utils,上层只关心结果,不关心具体的实现逻辑

    面向接口编程:

    当上层需要底层的某项服务时,将服务抽象成一个接口,上层持有这个接口,而不是具体的类,那么当底层发生了改变或是实现的时候,上层只需要实例化对应的新实现类即可,如果把这层实例化也作为接口去作,那么上层完全不用改变就能拥抱变化。

    依赖注入:

    横向的业务代码或者功能实现可以进行依赖注入的方式来达到解耦的目的。

    工程结构解耦:

    结构的解耦其实一般针对应用的整体业务而言进行的一个"分Module"操作,根据业务类型的横向拆分,业务属性的纵向拆分以及功能SDK下沉。

    2、组件module gradle管理

    • 在根目录下建立一个config.gradle文件
    • 编写对应的依赖常量代码
    • 在app module 的build.gradle中引用
    • 注意,如果想要在别的.gradle中使用声明的这些常量,一定要在抽取的xx.gradle文件中将对应的代码块用"ext"进行包裹

    config.gradle

    ext {
        android = [
                compileSdkVersion: 28,
                targetSdkVersion : 28,
                minSdkVersion    : 21,
        ]
    
        version = [
                retrofitSdkVersion      : "2.4.0",
                androidSupportSdkVersion: "28.0.0",
                butterknifeSdkVersion   : "8.8.1",
                espressoSdkVersion      : "3.0.1",
                canarySdkVersion        : "1.5.4",
                glideSdkVersion         : "4.8.0"
        ]
    
    
        dependencies = [
                //support
                "appcompat-v7"                : "com.android.support:appcompat-v7:${version["androidSupportSdkVersion"]}",
                "design"                      : "com.android.support:design:${version["androidSupportSdkVersion"]}",
                "support-v4"                  : "com.android.support:support-v4:${version["androidSupportSdkVersion"]}",
                "cardview-v7"                 : "com.android.support:cardview-v7:${version["androidSupportSdkVersion"]}",
                "annotations"                 : "com.android.support:support-annotations:${version["androidSupportSdkVersion"]}",
        ]
    }
    

    在工程根目录的build.gradle中加上如下代码:

    apply from: "config.gradle"
    

    在app module中的build.gradle中引用(其他组件module中引用类似):

    apply plugin: 'com.android.application'
    
    
    android {
    
        compileSdkVersion rootProject.ext.android["compileSdkVersion"]
        defaultConfig {
            applicationId "cn.com.xxx"
            minSdkVersion rootProject.ext.android["minSdkVersion"]
            targetSdkVersion rootProject.ext.android["targetSdkVersion"]
            versionName "1.0.0"
            versionCode getVersionCode(versionName)
        }
    }
    
    
    dependencies {
        implementation fileTree(include: ['*.jar'], dir: 'libs')
        implementation rootProject.ext.dependencies["appcompat-v7"]
        implementation rootProject.ext.dependencies["cardview-v7"]
        implementation rootProject.ext.dependencies["support-v4"]
        implementation rootProject.ext.dependencies["design"]
        implementation rootProject.ext.dependencies["annotations"]
        implementation rootProject.ext.dependencies["constraint-layout"]
        implementation rootProject.ext.dependencies["arch-lifecycle"]
        implementation rootProject.ext.dependencies["FlycoTabLayout_Lib"]
        implementation rootProject.ext.dependencies["FlycoPageIndicator_Lib"]
        implementation rootProject.ext.dependencies["nineoldandroids"]
        implementation rootProject.ext.dependencies["jiecaovideoplayer"]
        implementation rootProject.ext.dependencies["SmartRefreshLayout"]
    }
    

    如此以后在查看或者更换依赖的时候也方便查看和维护,注意在进行依赖的过程中,因为依赖不同的三方,可能会出现重复依赖相同库而版本不一致的情况,这里有两种解决办法,一种是在对应依赖的三方中剔除对应的pom依赖,如:

    api('com.facebook.fresco:fresco:0.10.0') {
           exclude module: 'support-v4'
    }
    

    另外一种是强制依赖的相同库的版本,如:

    configurations.all {
        resolutionStrategy.eachDependency { DependencyResolveDetails details ->
            def requested = details.requested
            if (requested.group == 'com.android.support') {
                if (requested.name.startsWith("support-") ||
                        requested.name.startsWith("animated") ||
                        requested.name.startsWith("cardview") ||
                        requested.name.startsWith("design") ||
                        requested.name.startsWith("gridlayout") ||
                        requested.name.startsWith("recyclerview") ||
                        requested.name.startsWith("transition") ||
                        requested.name.startsWith("appcompat")) {
                    details.useVersion SUPPORT_LIB_VERSION
                } else if (requested.name.startsWith("multidex")) {
                    details.useVersion OTHER_VERSION.multiDex
                }
            }
        }
    }
    

    3、组件路由

    路由其实是组件化的核心组件,网上也有很多优秀的开源库,这里就直接使用阿里的开源库ARouter,地址如下:

    https://github.com/alibaba/ARouter

    ARouter配置

    1.每一个模块都必须引入compiler sdk,只需在base模块中依赖 api sdk

    base module

    api   "com.alibaba:arouter-api:1.4.1"
    

    其他组件 module

    annotationProcessor "com.alibaba:arouter-compiler:1.2.2"
    

    2.每一个module 的分组都必须不同,分组就是path的第一个"/"与第二个"/"之间。

    3.每个module中的build.gradle中都要加入如下配置

    android {
        defaultConfig {
            
            javaCompileOptions {
                annotationProcessorOptions {
                    includeCompileClasspath = true
                    arguments = [AROUTER_MODULE_NAME: project.getName()]
                }
            }
        }
    }
    

    ARouter使用

    在common-service模块下建一个router包和一个service包,router包存放路由相关代码,service包存放各模块对外提供的服务相关代码,不同的模块提供的服务代码放在service包下面不同的子包中

    image.png

    1.ModulePath用来存在组件的路由地址,一级路由用模块名,二级路由用“module”(无特别含义,ARouter要求至少要有二级路由),根据实际情况确定是否需要三级路由

    @StringDef(
           ModulePath.LOGIN,
           ModulePath.MAIN
    )
    annotation class ModulePath {
    
        companion object {
    
            /**
             * 登录模块
             */
            const val LOGIN = "/login/module"
    
            /**
             * 首页模块
             */
            const val MAIN = "/main/module"
        }
    }
    

    2.RouterManager用来提供各模块间的路由和路由参数的解析,每个模块的开发者应该将该模块的路由跳转的代码写在RouterManager来提供别的模块跳转到该模块

    object RouterManager {
    
        fun goMain(index: Int) {
            val params = HashMap<String, Any>()
                    .apply {
                        put("position", index)
                    }
            postcard(ModulePath.MAIN, "tabSelect", params.toJson())
                    .navigation()
        }
    
        private fun postcard(@ModulePath module: String, target: String, params: String): Postcard {
            return ARouter.getInstance().build(module)
                    .withString(TARGET, target)
                    .withString(PARAMS, params)
        }
    }
    

    3.ServiceName用来存放组件对外提供服务的地址

    @StringDef(
            ServiceName.SERVICE_LOGIN,
            ServiceName.SERVICE_SHARE
    )
    annotation class ServiceName {
        companion object {
            /**
             * 登录信息服务
             */
            const val SERVICE_LOGIN = "/login/user"
            /**
             * 分享服务
             */
            const val SERVICE_SHARE = "/share/service"
        }
    }
    

    4.ServiceManager用来存放组件对外提供服务的方法,若模块开发者对其他模块需暴露自己的服务,需要将服务相关代码写在ServiceManager中,并加上注释

    object ServiceManager {
    
        /**
         * 获取通用服务
         */
        fun <T> getService(service: Class<out T>): T? {
            return ARouter.getInstance().navigation(service)
        }
    
        /**
         * 获取个人信息服务
         */
        fun getUserInfo(): IAppUserInfo? {
            return getService(IAppUserInfo::class.java)
        }
    
        /**
         * 获取main模块服务
         */
        fun getMainService(): IMainService? {
            return getService(IMainService::class.java)
        }
    }
    

    5.若一个模块要对外暴露自己的服务,需要在service包下新建自己的包,一般以模块名作为报名,在该包下提供服务的接口和相关常量

    interface IShareService : IProvider {
       
        fun showShareDialog(fragmentManager: FragmentManager, shareType: Int, shareParams: HashMap<String, Any>, miniProgramShareParams: HashMap<String, Any>)
    
        fun getShareImageUrl(shareParams: HashMap<String, Any>, consumer: Consumer<String>, error: Consumer<Throwable>): Disposable
    }
    

    6.各业务模块通过RouterManager来跳转

    RouterManager.goGoodsDetail(model.pitemId, model.exhibitionParkType)
    

    7.各业务模块通过ServiceManager调用服务

    private val appUserInfo by lazy {
        ServiceManager.getUserInfo()
    }
    

    4、单独调试

    当工程被拆分为组件化的时候,那么Module的单独调试就显得尤为重要,无论是对问题的跟踪还是业务线的并行开发,都需要工程具备单独运行调试的能力。这里单独调试同样是对gradle的操作,通过对编译脚本的编写来达到组件单独运行的目的。

    1.新建appConfig.gradle对需要单独运行的Module抽取变量进行记录:

    ext {
    
        app = [
                versionName: "1.5.0"
        ]
    
        runAlone = [
                login          : false,
                mine           : false,
                main           : false
        ]
    
    }
    

    并在工程的build.gradle中apply

    apply from: "appConfig.gradle"
    

    2.在对应module的编译脚本文件中添加判断module是以lib形式依赖还是以app方式进行依赖,如下代码:

    def runAlone = rootProject.ext.runAlone.ssbb.toBoolean()
    
    if (runAlone) {
        apply plugin: 'com.android.application'
    } else {
        apply plugin: 'com.android.library'
    }
    

    添加Application中一些必要的元素,清单文件Manifest.xml文件,但是这个xml文件是组件在单独运行的过程中所需要的,所以这里要放到一个runalone的目录下,在java目录下建一个runalone包,存放该组件单独编译时需要的代码:

    image.png

    同时在此基础上通过编译脚本配置单独运行时获取的Android相关文件以及在作为library时剔除相关文件:

    android {
        sourceSets {
            main {
                if (runAlone) {
                    manifest.srcFile 'src/main/runAlone/AndroidManifest.xml'
                } else {
                    manifest.srcFile 'src/main/AndroidManifest.xml'
                    java{
                        exclude 'src/main/runAlone/*'
                    }
                }
            }
        }
    }
    

    单独调试时增加applicationId,集成调试时移除

    android {
        defaultConfig {
            if (runAlone) {
                applicationId "com.jokerwan.baby"
            }
        }
    }
    

    根据以上配置,就可以使得业务模块单独运行起来,调试起来非常方便,但我们调用接口都是需要cookie的,也就是单独编译调试时是需要先登录的,继续往下看

    3.修改业务组件依赖关系:

    业务组件依赖基础业务组件common和基础服务组件common_service,再根据单独编译时是否需要依赖其他组件对相应的组件进行依赖:

    dependencies {
        kapt rootProject.ext.dependencies["arouter-compiler"]
    
        implementation project(path: ':common')
        implementation project(path: ':common_service')
    
        if (runAlone) {
            implementation project(path: ':login')
            implementation project(path: ':exhibition')
            implementation project(path: ':share')
        }
    }
    

    4.模块单独编译打包时登录处理:

    image.png

    在base模块新建一个delayaction,用来处理需要验证某些条件后才能出发的动作,比如登录后才能跳转到某个Activity,没有登录时先跳转到登录页面,SingleCall具体实现逻辑如下,通过单例存储一个Action,在Action执行前先判断执行条件,执行条件没通过就调用doMakeValid()去使执行条件有效,执行条件通过后再手动触发call()来执行Action

    /**
     * Created by JokerWan on 2019-10-31.
     * Function: 延迟任务处理的类需要实现的接口
     */
    public interface Action {
    
        /**
         * 前置条件通过后执行的回调
         */
        void call();
    }
    
    /**
     * Created by JokerWan on 2019-10-31.
     * Function: 一个执行单元
     */
    public class CallUnit {
    
        //目标行为
        private Action action;
        //验证模型队列
        private Queue<Condition> conditionQueue = new ArrayDeque<>();
        //上一个执行的Condition
        private Condition lastCondition;
    
        public CallUnit() {
        }
    
        public CallUnit(Action action) {
            this.action = action;
        }
    
        public Action getAction() {
            return action;
        }
    
        public void setAction(Action action) {
            this.action = action;
        }
    
        public Condition getLastCondition() {
            return lastCondition;
        }
    
        public void setLastCondition(Condition lastCondition) {
            this.lastCondition = lastCondition;
        }
    
        public CallUnit addCondition(Condition condition) {
            conditionQueue.add(condition);
            return this;
        }
    
        public Queue<Condition> getConditionQueue() {
            return conditionQueue;
        }
    
    }
    
    /**
     * Created by JokerWan on 2019-10-31.
     * Function: 条件验证模型
     */
    public interface Condition {
    
        /**
         * 是否满足检验器的要求,如果不满足的话,则执行doMakeValid方法。如果满足,则执行目标action
         */
        boolean check();
    
        /**
         * 不满足检验器时执行
         */
        void doMakeValid();
    }
    
    /**
     * Created by JokerWan on 2019-10-31.
     * Function: Condition与Action管理
     */
    public class SingleCall {
    
        private CallUnit callUnit = new CallUnit();
    
        /**
         * 获取单例
         */
        public static SingleCall getInstance() {
            return SingletonHolder.mInstance;
        }
    
        private static class SingletonHolder {
            private static SingleCall mInstance = new SingleCall();
        }
    
        public SingleCall addAction(Action action) {
            clear();
            callUnit.setAction(action);
            return this;
        }
    
        /**
         * 添加需要验证的条件
         *
         * @param condition 需验证的条件
         * @return this
         */
        public SingleCall addCondition(Condition condition) {
            // 只添验证不通过的
            if (condition.check()) {
                return this;
            }
            callUnit.addCondition(condition);
            return this;
        }
    
        public void call() {
    
            // 上一个condition没有验证通过的话,是不允许再发起call的
            if (callUnit.getLastCondition() != null && !callUnit.getLastCondition().check()) {
                return;
            }
    
            // 执行action
            if (callUnit.getConditionQueue().size() == 0 && callUnit.getAction() != null) {
                callUnit.getAction().call();
                // 清空
                clear();
            } else {
                // 执行验证。
                Condition condition = callUnit.getConditionQueue().poll();
                callUnit.setLastCondition(condition);
                if (condition != null) {
                    condition.doMakeValid();
                }
            }
    
        }
    
        /**
         * 清空回调,建议在Activity/Fragment生命周期的onDestroy
         * 回调中调用该方法避免内存泄漏
         */
        public void clear() {
            callUnit.getConditionQueue().clear();
            callUnit.setAction(null);
            callUnit.setLastCondition(null);
        }
    }
    

    下面我们以baby模块的代码来具体看下在模块单独打包时是如何获取登录状态

    首先在login模块中加入如下代码

    // 登录成功之后调用
        private fun finishLogin() {
            var goMain = false
            // 'goMain' set value
            if (goMain) {
                RouterManager.goMain(0, "Login")
            } else {
                // 在这里触发call()来执行登录成功之后的Action
                SingleCall.getInstance().call()
            }
            finish()
        }
    

    xxx模块runalone.xxx包下

    image.png

    BabyApp继承WApp,WApp是定义在common模块下的通用Application,包括一些通用的第三方组件的初始化

    class BabyApp : WApp()
    
    open class WApp : Application() {
    
        override fun onCreate() {
            super.onCreate()
            app = this
    
            initARouter()
            initUpgradeManager()
            initThirdServiceInBackground()
            initRefreshLayout()
            initUmengPush()
            initLeakCanary()
        }
    }
    

    LoginCondition是登录验证条件

    class LoginCondition : Condition {
    
        /**
         * 验证条件
         */
        override fun check(): Boolean {
            val userInfo =
                ARouter.getInstance().build(ServiceName.SERVICE_LOGIN).navigation() as IAppUserInfo
            return userInfo.getId() != 0L
        }
    
        /**
         * 执行验证条件
         */
        override fun doMakeValid() {
            RouterManager.goLoginWillBack("sst")
        }
    }
    

    BabyRouterActivity是登录跳转中间类,主要作用是路由转发或者提供装载模块Fragment容器(该模块只有Fragment,没有Activity)

    class BabyRouterActivity : BaseActivity(),Action{
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            StatusBarUtil.setStatusBarColorWhite(this)
    
            // 执行登录条件验证
            SingleCall.getInstance()
                .addAction(this)
                .addCondition(LoginCondition())
                .call()
        }
    
       /**
         * onDestroy()时清空SingleCall
         */
        override fun onDestroy() {
            super.onDestroy()
            SingleCall.getInstance().clear()
        }
    
        /**
         * 登录条件验证通过的回调
         */
        override fun call() {
            startActivity(Intent(this, BabyActivity::class.java))
            finish()
            
            // addFragment(android.R.id.content, MineFragment())
        }
    }
    

    当验证条件通过后会回调call()方法,在call()方法里可以跳转到模块的Activity或者直接用此Activity装载Fragment

    5、整体调试

    将appConfig.gradle中的模块单独调试变量全部改为false

    根据上述方案,app壳其实只需要依赖业务组件即可:

    dependencies {
        
        implementation project(path: ':common')
        implementation project(path: ':common_service')
    
        if (!rootProject.ext.runAlone.login.toBoolean()) {
            implementation project(path: ':login')
        }
    
        if (!rootProject.ext.runAlone.mine.toBoolean()) {
            implementation project(path: ':mine')
        }
    }
    

    6、资源名冲突

    color,shape,drawable,图片资源,布局资源,或者anim资源等等,都有可能造成资源名称冲突。有时候大家负责不同的模块,如果不是按照统一规范命名,则会偶发出现该问题

    可以通过设置 resourcePrefix 来避免。设置了这个值后,你所有的资源名必须以指定的字符串做前缀,否则会报错。但是 resourcePrefix 这个值只能限定 xml 里面的资源,并不能限定图片资源,所有图片资源仍然需要你手动去修改资源名。

    android {
        // 所有xml资源命名以 "login_" 开头、否则编译报红
        resourcePrefix "login_"
    }
    

    7、组件Application初始化

    自定义 Application 需要声明在 AndroidManifest.xml 中。其次,每个 Module 都有该清单文件,但是最终的 APK 文件只能包含一个。因此,在构建应用时,Gradle 构建会将所有清单文件合并到一个封装到 APK 的清单文件中。

    合并的优先级是:

    App Module > Library Module

    合并的规则:

    image.png

    结合我们的情况,是值 A 合并值 B,会产生冲突错误:

    Execution failed for task ':app:processDebugManifest'.
    > Manifest merger failed : Attribute application@name value=(com.baseres.BaseApplication) from AndroidManifest.xml:8:9-51
        is also present at [:carcomponent] AndroidManifest.xml:14:9-55 value=(com.carcomponent.CarApplication).
        Suggestion: add 'tools:replace="android:name"' to <application> element at AndroidManifest.xml:7:5-24:19 to override.
    

    错误信息中给出了解决建议,在高优先级的 App Module 中使用 tools:replace="android:name",但这样做是直接用值 A 替换了值 B,并非我们想要的结果。另外再推荐给大家一个方法,打开 App Module 的 AndroidManifest.xml 文件,选择下方 Merged Manifest 选项卡,可以看到预合并结果。

    解决方法一:每个业务模块通过Arouter暴露出模块初始化服务,app模块可在适当的时机(为了增强秒开体验,尽量别在app模块的Application中初始化,可以在MainActivity中进行初始化或者在跳转业务模块前初始化)调用各模块的服务进行初始化

    解决方法二:通过反射在app模块的Application的onCreate()方法中调用组件Application的初始化方法

    1.在base模块中新增BaseAPP

    public abstract class BaseApp extends Application {
        /**
         * Application 初始化
         */
        public abstract void initModuleApp(Application application);
        private static Context appContext;
    
        @Override
        public void onCreate() {
            super.onCreate();
            appContext = getApplicationContext();
        }
    
        public static Context getAppContext() {
            return appContext;
        }
    }
    

    2.在live模块中的LiveApplication继承BaseApp实现initModuleApp()方法,并在此方法中做初始化操作,作为library时初始化操作在initModuleApp()方法中,作为独立app时,初始化操作在onCreate()中。

    public class LiveApplication extends BaseApp {
    
        @Override
        public void onCreate() {
            super.onCreate();
        }
    
        @Override
        public void initModuleApp(Application application) {
            // init
        }
    }
    

    3.在base模块中增加AppConfig类,用来配置需要初始化的组件Application

    public class AppConfig {
        private static final String LiveApp = "cn.com.live.LiveApplication";
    
        public static String[] moduleApps = {
                LiveApp
        };
    }
    

    4.在app的Application的onCreate()方法中通过反射初始化在AppConfig中声明类全路径的组件Application类

    public class WApp extends BaseApp {
    
        @Override
        public void onCreate() {
            super.onCreate();
    
            initModuleApp(this);
        }
        
        @Override
        public void initModuleApp(Application application) {
            for (String moduleApp : AppConfig.moduleApps) {
                try {
                    Class clazz = Class.forName(moduleApp);
                    SBBaseApp baseApp = (SBBaseApp) clazz.newInstance();
                    baseApp.initModuleApp(this);
                } catch (ClassNotFoundException e) {
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                } catch (InstantiationException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    

    8、组件代码混淆

    方法一:直接在app模块的proguard-rules.pro中编写所有组件的混淆规则

    优点:简单无脑

    缺点:若app模块依赖的组件很多,则proguard-rules.pro中混淆规则庞大不利于维护,使用app模块编写所有混淆命令是基于业务模块当中不再编写混淆命令为前提,所以在打包将业务模块上传到私有仓库时,业务模块都是不开启混淆功能的!

    但是

    上述结论都是建立在以implementation或者api形式依赖的前提下,开发阶段我们是以

    implementation project(':live')
    

    这种形式进行依赖的,你会发现当以这种形式进行依赖时,不管业务模块minifyEnabled是true还是false,只要app模块写上了正确的混淆规则那么程序都能正常运行!

    方法二:各个业务组件单独编写混淆规则(推荐)

    优点:各组件自己配置混淆文件,易于维护

    在模块中的build.gradle中配置

    android {
        buildTypes {
            release {
                consumerProguardFiles 'proguard-rules.pro'
            }
        }
    }
    

    使用这种配置最大的一个好处就是业务模块的是否混淆完全由app模块来决定,这种配置有一个非常重要的关键点就是不能设置minifyEnabled true,因为设置为true之后业务模块是否混淆的控制权将只能由该模块自身决定,app模块将无法控制业务模块的混淆与否而且设置为true之后还必须额外配置

    proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    

    9、组件库的独立发布和维护

    原有拆分的本地组件彻底分离出去,采取独立发布和维护的方式迭代更新。

    相关文章

      网友评论

        本文标题:Android组件化工程结构以及项目具体实施方案

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