AwesomeGithub组件化探索之旅

awesome_github_page_header_image.jpeg

以前一直据说过组件化开发,并且面试也有这方面的提问,但都不曾有涉及具体的项目。因此就萌生了基于Github的开放Api,并使用组件化的方式来从零搭建一个Github客户端,起名为AwesomeGithubhtml

在这里对组件化开发进行一个总结,同时也但愿可以帮助别人更好的理解组件化开发。java

先来看下项目的总体效果android

awesome_github.png

下面是项目的结构git

awesome_github_project.jpg

为什么要使用组件化

  1. 对于传统的开发模式,一个app下面是包含项目的所有页面模块与逻辑。这样项目一旦迭代时间过长,业务模块逐渐增长,相应的业务逻辑复杂度也成指数增长。模块间的互相调用频繁,这样一定会致使模块间的耦合增长,业务逻辑嵌套程度加深。一旦修改其中一个模块,可能就牵一发动全身了。
  2. 传统的开发模式不利于团队的集体开发合做,由于每一个开发者都是在同一个app模块下开发。这样致使的问题是,不能预期每一个开发者所会修改到的具体代码部分,即所可以修改的代码区域。由于模块耦合在一块儿,涉及的区域不可预期,致使不一样开发者会修改同一个文件或者同一段代码逻辑,从而致使异常冲突。
  3. 传统开发模式不利于测试,每次迭代都要将项目总体测试一遍。由于在同一个app下面代码是缺少约束的,你不能保证只修改了迭代过程当中所涉及的需求逻辑。

以上问题随着项目的迭代周期的增大,会表现的愈来愈明显。那么使用组件化又可以解决什么问题了?程序员

组件化可以解决的问题

  1. 组件化开发是将各个相关功能进行分离,分别独立成一个单独可运行的app,而且组件之间不能相互直接引用。这样就减小了代码耦合,达到业务逻辑分层效果。
  2. 组件化能够提升团队协做能力,不一样的人员能够开发不一样的组件,保证不一样开发人员互不影响。
  3. 组件化将app分红多个可单独运行的子项目,能够用本身独立的版本,能够独立编译,打包、测试与部署。这样不只能够提升单个模块的编译速度,同时也能够提升测试的效率。
  4. 组件化能够提升项目的灵活性,app能够按需加载所要有的组件,提升app的灵活性,能够快速生成可定制化的产品。

如今咱们已经了解了组件化的做用,但要实现组件化,达到其做用,必须解决实现组件化过程当中所遇到的问题。github

组件化须要解决的问题

  1. 组件单独运行
  2. 组件间数据传递
  3. 主项目使用组件中的Fragment
  4. 组件间界面的跳转
  5. 组件解耦

以上是实现组件化时所遇到的问题,下面我会结合AwesomeGithub来具体说明解决方案。web

组件单独运行

组件的建立,能够直接使用library的方式进行建立。只不过在建立完以后,要让组件达到能够单独运行调试的地步,还须要进行相关配置。面试

运行方式动态配置

首先,当建立完library时,在build.gradle中能够找到这么一行代码缓存

apply plugin: 'com.android.library'

这是gradle插件所支持的一种构建类型,表明能够将其依赖到主项目中,构建后输出aar包。这种方式对于咱们将组件依赖到主项目中彻底吻合的。app

而gradle插件的另外一种构建方式,能够在主项目的build.gradle中看到这么一行代码

apply plugin: 'com.android.application'

这表明在项目构建后会输出apk安装包,是一个独立可运行的项目。

明白了gradle的这两种构建方式以后,咱们接下须要作的事也很是明了:须要将这两种方式进行动态配置,保证组件在主项目中以library方式存在,而本身单独的时候,则以application的方式存在。

下面我以AwesomeGithub中的login组件为例。

首先咱们在根项目的gradle.properties中添加addLogin变量

addLogin = true

而后在login中的build.gradle经过addLogin变量来控制构建方式

if (addLogin.toBoolean()) {
    apply plugin: 'com.android.library'
} else {
    apply plugin: 'com.android.application'
}

这样就实现了对login的构建控制,可单独运行,也可依赖于app项目。

ApplicationId与AndroidManifest

除了修改gradle的构建方式,还须要动态配置ApplicationId与AndroidManifest文件。

有了上面的基础,实现方式也很简单。

能够在defaultConfig中增长对applicationId的动态配置

defaultConfig {
        if (!addLogin.toBoolean()) {
            applicationId "com.idisfkj.awesome.login"
        }
        minSdkVersion Versions.min_sdk
        targetSdkVersion Versions.target_sdk
        versionCode Versions.version_code
        versionName Versions.version_name
 
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

而AndroidManifest文件能够经过sourceSets来配置

sourceSets {
        main {
            if (addLogin.toBoolean()) {
                manifest.srcFile 'src/main/AndroidManifest.xml'
            } else {
                manifest.srcFile 'src/main/manifest/AndroidManifest.xml'
            }
        }
    }

awesome_github_login.jpg

同时addLogin也能够做用于app,让login组件可配置依赖

awesome_github_login_main.jpg

这样login组件就能够独立于app进行单独构建、打包、调试与运行。

组件间的数据传递

因为组件与组件、项目间是不能直接使用类的相互引用来进行数据的传递,因此为了解决这个问题,这里经过一个公共库来作它们之间调用的桥梁,它们不直接拿到具体的引用对象,而是经过接口的方式来获取所须要的数据。

AwesomeGithub中我将其命名为componentbridge,各个组件都依赖于该公共桥梁,经过该公共桥梁各个组件间能够轻松的实现数据传递。

awesome_github_component_bridge.jpg

上图圈起来的部分都是componentbridge的重点,也是公共桥梁实现的基础。下面来分别详细说明。

BridgeInterface

这是公共桥梁的底层接口,每个组件要向外实现本身的桥梁都要实现这个接口。

interface BridgeInterface {

    fun onClear() {}
}

内部很简单,只有一个方式onClear(), 用来进行数据的释放。

BridgeStore

用来作数据存储,对桥梁针对不一样的key进行缓存。避免桥梁内部的实例屡次建立。具体实现方式以下:

class BridgeStore {
 
    private val mMap = HashMap<String, BridgeInterface>()
 
    fun put(key: String, bridge: BridgeInterface) {
        mMap.put(key, bridge)?.onClear()
    }
 
    fun get(key: String): BridgeInterface? = mMap[key]
 
    fun clear() {
        for (item in mMap.values) {
            item.onClear()
        }
        mMap.clear()
    }
}

Factory

桥梁的实例构建工厂,默认提供经过反射的方式来实例化不一样的类。Factory接口只提供一个create方法,实现方式由子类自行解决

interface Factory {
 
    fun <T : BridgeInterface> create(bridgeClazz: Class<T>): T
}

AwesomeGithub中提供了经过反射方式来实例化不一样类的具体实现NewInstanceFactory

class NewInstanceFactory : Factory {
 
    companion object {
        val instance: NewInstanceFactory by lazy { NewInstanceFactory() }
    }
 
    override fun <T : BridgeInterface> create(bridgeClazz: Class<T>): T = try {
        bridgeClazz.newInstance()
    } catch (e: InstantiationException) {
        throw RuntimeException("Cannot create an instance of $bridgeClazz", e)
    } catch (e: IllegalAccessException) {
        throw RuntimeException("Cannot create an instance of $bridgeClazz", e)
    }
 
}

Factory的做用是经过抽象的方式来获取所须要类的实例,至于该类如何实例化,将经过create方法自行实现。

Provider

Provider是提供桥梁的注册与获取各个组件暴露的接口实现。经过register来统一各个组件向外暴露的桥梁类,最后再经过getBridge来获取具体的桥梁类,而后调用所需的相关方法,最终达到组件间的数据传递。

来看下BridgeProviders的具体实现

class BridgeProviders {
 
    private val mProvidersMap = HashMap<Class<*>, BridgeProvider>()
    private val mBridgeMap = HashMap<Class<*>, Class<*>>()
    private val mDefaultBridgeProvider = BridgeProvider(NewInstanceFactory.instance)
 
    companion object {
        val instance: BridgeProviders by lazy { BridgeProviders() }
    }
 
    fun <T : BridgeInterface> register(
        clazz: Class<T>,
        factory: Factory? = null,
        replace: Boolean = false
    ) = apply {
        if (clazz.interfaces.isEmpty() || !clazz.interfaces[0].interfaces.contains(BridgeInterface::class.java)) {
            throw RuntimeException("$clazz must implement BridgeInterface")
        }
        // 1. get contract interface as key, and save implement class to map value.
        // 2. get contract interface as key, and save bridgeProvider of implement class instance
        // to map value.
        clazz.interfaces[0].let {
            if (mProvidersMap[it] == null || replace) {
                mBridgeMap[it] = clazz
                mProvidersMap[it] = if (factory == null) {
                    mDefaultBridgeProvider
                } else {
                    BridgeProvider(factory)
                }
            }
        }
    }
 
    fun <T : BridgeInterface> getBridge(clazz: Class<T>): T {
        mProvidersMap[clazz]?.let {
            @Suppress("UNCHECKED_CAST")
            return it.get(mBridgeMap[clazz] as Class<T>)
        }
        throw RuntimeException("$clazz subClass is not register")
    }

    fun clear() {
        mProvidersMap.clear()
        mBridgeMap.clear()
        mDefaultBridgeProvider.bridgeStore.clear()
    }
}

每次register以后都会保存一个BridgeProvider实例,若是没有实现自定义的Factory,将会使用默认是mDefaultBridgeProvider,它内部使用的就是默认的NewInstanceFactory

class BridgeProvider(private val factory: Factory) {
 
    val bridgeStore = BridgeStore()
 
    companion object {
        private const val DEFAULT_KEY = "com.idisfkj.awesome.componentbridge"
    }
 
    fun <T : BridgeInterface> get(key: String, bridgeClass: Class<T>): T {
        var componentBridge = bridgeStore.get(key)
        if (bridgeClass.isInstance(componentBridge)) {
            @Suppress("UNCHECKED_CAST")
            return componentBridge as T
        }
        componentBridge = factory.create(bridgeClass)
        bridgeStore.put(key, componentBridge)
        return componentBridge
    }
 
    fun <T : BridgeInterface> get(bridgeClass: Class<T>): T =
        get(DEFAULT_KEY + "@" + bridgeClass.canonicalName, bridgeClass)
}

注册完以后就能够在任意的组件中经过调用桥梁的getBridge来获取组件向外暴露的方法,从而达到数据的传递。

咱们来看下具体的使用示例。

AwesomeGithub项目使用的是Github Open Api,用到的接口基本都要AuthorizationBasic或者是AccessToken,而为了让每个组件在调用接口时都可以正常获取到AuthorizationBasic或者AccessToken,因此提供了一个AppBridge与AppBridgeInterface来向外暴露这些数据,实现以下:

interface AppBridgeInterface: BridgeInterface {
 
    /**
     * 获取用户的Authorization Basic
     */
    fun getAuthorizationBasic(): String?
 
    fun setAuthorizationBasic(authorization: String?)
 
    /**
     * 获取用户的AccessToken
     */
    fun getAccessToken(): String?
 
    fun setAccessToken(accessToken: String?)
}
class AppBridge : AppBridgeInterface {
 
    override fun getAuthorizationBasic(): String? = App.AUTHORIZATION_BASIC
 
    override fun setAuthorizationBasic(authorization: String?) {
        App.AUTHORIZATION_BASIC = authorization
    }
 
    override fun getAccessToken(): String? = App.ACCESS_TOKEN
 
    override fun setAccessToken(accessToken: String?) {
        App.ACCESS_TOKEN = accessToken
    }
 
}

有了上面的桥梁接口,接下来须要作的是先在App主项目中进行注册

private fun registerBridge() {
        BridgeProviders.instance.register(AppBridge::class.java, object : Factory {
            override fun <T : BridgeInterface> create(bridgeClazz: Class<T>): T {
                @Suppress("UNCHECKED_CAST")
                return AppBridge() as T
            }
        })
            .register(HomeBridge::class.java)
            .register(UserBridge::class.java)
            .register(ReposBridge::class.java)
            .register(FollowersBridge::class.java)
            .register(FollowingBridge::class.java)
            .register(NotificationBridge::class.java)
            .register(SearchBridge::class.java)
            .register(WebViewBridge::class.java)
    }

在注册AppBridge时使用的是自定义的Factory,这里只是为了简单展现自定义的Factory的使用,其实没有特殊需求能够与后面的bridge同样直接调用regiser进行注册。

注册完了以后就能够直接在须要的地方进行调用。首先在登陆组件中将获取到的AuthorizationBasic或者AccessToken进行保存,以便被以后的组件进行调用。

以AccessToken为例,在login组件中的核心调用代码以下:

fun getAccessTokenFromCode(code: String) {
        showLoading.value = true
        repository.getAccessToken(code, object : RequestCallback<Response<ResponseBody>> {
            override fun onSuccess(result: ResponseSuccess<Response<ResponseBody>>) {
                try {
                    appBridge.setAccessToken(
                        result.data?.body()?.string()?.split("=")?.get(1)?.split("&")?.get(
                            0
                        )
                    )
                    getUser()
                } catch (e: IOException) {
                    e.printStackTrace()
                }
            }
 
            override fun onError(error: ResponseError) {
                showLoading.value = false
            }
        })
    }

如上所示,只需调用appBridge.setAccessToken将数据进行保存;而appBridge能够经过以下获取

appBridge = BridgeProviders.instance.getBridge(AppBridgeInterface::class.java)

如今已经有了AccessToken数据,为了不每次调用接口都手动加入AccessToken,可使用okhttp的Interceptor,即在network组件中进行统一加入。

class GithubApiInterceptor : Interceptor {

    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()

        val appBridge =
            BridgeProviders.instance.getBridge(AppBridgeInterface::class.java)
        Timber.d("intercept url %s %s %s", request.url(), appBridge.getAuthorizationBasic(), appBridge.getAccessToken())

        val builder = request.newBuilder()
        val authorization =
            if (!TextUtils.isEmpty(appBridge.getAuthorizationBasic())) "Basic " + appBridge.getAuthorizationBasic()
            else "token " + appBridge.getAccessToken()
        builder.addHeader("Authorization", authorization)
        val response = chain.proceed(builder.build())
        Timber.d("intercept url %s, response %s ,code %d", request.url(), response.body().toString(), response.code())
        return response
    }
}

这样就完成了将AccessToken从login组件到network组件间的传递。

单个组件中调用

以上是主项目中集成了login组件,login组件会提供AuthorizationBasic或者AccessToken。那么对于单个组件(组件能够单独运行),为了让组件单独运行时也能调通相关的接口,在调用的时候加入正确的AuthorizationBasic或者AccessToken。须要提供默认的AppBridgeInterface实现类。我这里命名为DefaultAppBridge

class DefaultAppBridge : AppBridgeInterface {
 
    override fun getAuthorizationBasic(): String? = BuildConfig.AUTHORIZATION_BASIC
 
    override fun setAuthorizationBasic(authorization: String?) {
 
    }
 
    override fun getAccessToken(): String? = BuildConfig.ACCESS_TOKEN
 
    override fun setAccessToken(accessToken: String?) {
 
    }
}

里面具体的AuthorizationBasic与AccessToken值能够经过BuildConfig获取,而值的定义能够在local.properities中进行设置

AuthorizationBasic="xxxx"
AccessToken="xxx"

由于每一个组件都会依赖与桥梁componentbridge,因此将值配置到componentbridge的build中,具体以下:

android {
    compileSdkVersion Versions.target_sdk
    buildToolsVersion Versions.build_tools
 
    defaultConfig {
        minSdkVersion Versions.min_sdk
        targetSdkVersion Versions.target_sdk
        versionCode Versions.version_code
        versionName Versions.version_name
        buildConfigField "String", "AUTHORIZATION_BASIC", getProperties("AuthorizationBasic") + ""
        buildConfigField "String", "ACCESS_TOKEN", getProperties("AccessToken") + ""
 
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

    }
 
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
 
}

有了默认的组件桥梁实现,如今只需在对应的组件Application中进行注册便可。

例如项目中的followers组件,单独运行时使用DefaultAppBridge来达到接口的正常调用。

class FollowersApp : Application() {
 
    override fun onCreate() {
        super.onCreate()
        SPUtils.init(this)
        initTimber()
        initRouter()
        // register bridges
        BridgeProviders.instance.register(DefaultAppBridge::class.java)
            .register(DefaultWebViewBridge::class.java)
    }
 
    private fun initTimber() {
        if (BuildConfig.DEBUG) {
            Timber.plant(Timber.DebugTree())
        }
    }
 
    private fun initRouter() {
        if (BuildConfig.DEBUG) {
            ARouter.openLog()
            ARouter.openDebug()
        }
        ARouter.init(this)
    }
}

在组件单独运行时的Application中注册以后,单独运行时调用的就是local.properities中设置的值。即保证了组件正常单独运行。

以上是组件间数据传递的所有内容,即解决了组件间的数据传递也解决了组件单独运行时的默认数据调用问题。如需了解所有代码能够查看AwesomeGithub项目。

主项目使用组件中的Fragment

awesome_github_search.jpeg

AwesomeGithub主页有三个tab,分别是三个组件。这个三个组件是主页viewpager中的三个fragment。前面已经说了,在主项目中不能直接调用各个组件,那么组件中的fragment又该如何加入到主项目中呢?

其实也很简单,能够将获取fragment的实例看成为组件间的数据传递的一种特殊形式。那么有了上面的组件间数据传递的基础,实如今主项目中调用组件的fragment也瞬间简单了许多。借助的仍是桥梁componentbridge。

下面以主页的search为例

SearchBridgeInterface

首先在componentbridge中建立SearchBridgeInterface接口,而且实现默认的桥梁的BridgeInterface接口。

interface SearchBridgeInterface : BridgeInterface {
 
    fun getSearchFragment(): Fragment
}

其中就一个方法,用来向外提供SearchFragment的获取

接下来在search组件中实现SearchBridgeInterface的具体实现类

class SearchBridge : SearchBridgeInterface {
 
    override fun getSearchFragment(): Fragment = SearchFragment.getInstance()
 
}

而后回到主项目的Application中进行注册

BridgeProviders.instance.register(SearchBridge::class.java)

注册完以后,就能够在主项目的ViewPagerAdapter中进行获取SearchFragment实例

class MainViewPagerAdapter(fm: FragmentManager?) : FragmentPagerAdapter(fm) {
 
    override fun getItem(position: Int): Fragment = when (position) {
        0 -> BridgeProviders.instance.getBridge(SearchBridgeInterface::class.java).getSearchFragment()
        1 -> BridgeProviders.instance.getBridge(NotificationBridgeInterface::class.java)
            .getNotificationFragment()
        else -> BridgeProviders.instance.getBridge(UserBridgeInterface::class.java).getUserFragment()
    }
 
    override fun getCount(): Int = 3
}

主项目中调用组件中的Fragment就是这么简单,基本上与以前的数据传递时一致的。

组件间界面的跳转

有了上面的基础,可能会联想到使用处理Fragment方式来进行组件间页面的跳转。的确这也是一种解决方式,不过接下来要介绍的是另外一种更加方便与高效的跳转方式。

项目中使用的是ARouter,它是一个帮助App进行组件化改造的框架,支持模块间的路由、通讯与解藕。下面简单的介绍下它的使用方式。

首先须要去官网找到版本依赖,并进行导入。这里很少说,而后须要在你全部用到的模块中的build.gradle中添加如下配置

kapt {
    arguments {
        arg("AROUTER_MODULE_NAME", project.getName())
    }
}

记住只要该模块须要调用ARouter,就须要添加以上代码。配置完以后就能够开始使用。

下面我以项目中的webview组件为例,跳转到组件中的WebViewActivity

上面已经将相关依赖配置好了,首先须要在Application中进行ARouter初始化

private fun initRouter() {
        if (BuildConfig.DEBUG) {
            ARouter.openLog()
            ARouter.openDebug()
        }
        ARouter.init(this)
    }

再为WebViewActivity进行path定义

object ARouterPaths {
    const val PATH_WEBVIEW_WEBVIEW = "/webview/webview"
}

由于每个ARouter进行路由的时候,都须要配置一个包含两级的路径,而后将定义的路径配置到WebViewActivity中

@Route(path = ARouterPaths.PATH_WEBVIEW_WEBVIEW)
class WebViewActivity : BaseActivity<WebviewActivityWebviewBinding, WebViewVM>() {
 
    @Autowired
    lateinit var url: String
    @Autowired
    lateinit var requestUrl: String
 
    override fun getVariableId(): Int = BR.vm
 
    override fun getLayoutId(): Int = R.layout.webview_activity_webview
 
    override fun getViewModelInstance(): WebViewVM = WebViewVM()
 
    override fun getViewModelClass(): Class<WebViewVM> = WebViewVM::class.java
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        ARouter.getInstance().inject(this)
        viewModel.url.value = url
        viewModel.request(requestUrl)
    }
 
    override fun addObserver() {
        super.addObserver()
        viewModel.backClick.observe(this, Observer {
            finish()
        })
    }
 
    override fun onBackPressed() {
        if (viewDataBinding.webView.canGoBack()) {
            viewDataBinding.webView.goBack()
            return
        }
        super.onBackPressed()
    }
 
}

如上所示,在进行配置时,只需在类上添加@Route注解,而后再将定义的路径配置到path上。其中的@Autowired注解表明WebViewActivity在使用ARouter进行跳转时,接收两个参数,分别为url与requestUrl。

ARouter本质是解析注解,而后定位到参数,再经过原始的Intent中获取到传递过来的参数值。

有了上面的准备过程,最后剩下的就是调用ARouter进行页面跳转。这里为了统一调用方式,将其调加到桥梁中。

class WebViewBridge : WebViewBridgeInterface {
 
    override fun toWebViewActivity(context: Context, url: String, requestUrl: String) {
        ARouter.getInstance().build(ARouterPaths.PATH_WEBVIEW_WEBVIEW).with(
            bundleOf("url" to url, "requestUrl" to requestUrl)
        ).navigation(context)
    }
 
}

前面是定义的跳转路径,后面紧接的是页面传递的参数值。剩下的就是在别的组件中调用该桥梁,例如followers组件中的contentClick点击:

class FollowersVHVM(private val context: Context) : BaseRecyclerVM<FollowersModel>() {
 
    var data: FollowersModel? = null
 
    override fun onBind(model: FollowersModel?) {
        data = model
    }
 
    fun contentClick() {
        BridgeProviders.instance.getBridge(WebViewBridgeInterface::class.java)
            .toWebViewActivity(context, data?.html_url ?: "", "")
    }
}
更多ARouter的使用方式,读者能够自行查阅官方文档

AwesomeGithub项目中,组件化过程当中的主要难点与解决方案已经分析的差很少了。最后咱们来聊聊组件间的解藕优化。

组件解耦

组件化自己就是对项目进行解藕,因此若是要进一步进行优化,主要是对组件间的依赖或者资源等方面进行解藕。而对于组件间的依赖,尝试过在依赖的时候使用runtimeOnly。由于runtimeOnly能够避免依赖的组件在运行以前进行引用调用,它只会在项目运行时才可以正常的引用,这样就能够防止主项目中进行开发时直接引用依赖的组件。

可是,在实践的过程当中,若是项目中使用了DataBinding,此时使用runtimeOnly进行依赖组件,经过该方式依赖的组件在运行的过程当中会出现错误。

awesome_github_error.png

这是因为DataBinding须要在编译时生成对应资源文件。使用runtimeOnly会致使其缺失,最终在程序进行运行时找不到对应资源,致使程序异常。

固然若是没有使用DataBinding就不会有这种问题。这是组件依赖方面,下面再来讲说资源相关的。

因为不一样组件模块下能够引入相同命名的资源文件,为了防止开发过程当中不一样组件下相同名称的资源文件引用错乱,这里能够经过在不一样组件模块中的build.gradle中添加资源前缀。例如login组件中

awesome_github_resource.png

resourcePrefix表明login组件中的全部资源文件命名都必须以login_为前缀命名。若是没有编译器将会标红,并提示你正确的使用方式。这种方式能够必定程度上避免资源文件的乱用与错乱。

以上是AwesomeGithub组件化过程当中的整个探索经历。若是你想更深刻的了解其实现过程,强烈建议你直接查看项目的源码,毕竟语言上的描述是有限的,程序员就应该直接看代码才能更快更准的理解。

项目地址

AwesomeGithub: https://github.com/idisfkj/Aw...

若是这篇文章对你有所帮助,你能够顺手点赞、关注一波,这是对我最大的鼓励!

Android补给站.jpg

相关文章
相关标签/搜索