大型项目架构:全动态插件化+模块化+Kotlin+协程+Flow+Retrofit+JetPack+MVVM+极限瘦身+极限启动优化+架构示例+全网唯一

19,700 阅读16分钟

1213.webp

前言:十年寒窗磨一剑,六月沙场试锋芒,吃得苦中苦,方为人上人

(一)零反射,零HooK,全动态化,插件化框架,全网唯一结合启动优化的插件化架构
(二)零反射,零HooK,全动态化,插件化框架,全网唯一结合启动优化的插件化架构
(三)零反射,零HooK,全动态化,插件化框架,全网唯一结合启动优化的插件化架构
(四)零反射,零HooK,全动态化,插件化框架,全网唯一结合启动优化的插件化架构

(五) 大型项目架构:全动态插件化+模块化+Kotlin+协程+Flow+Retrofit+JetPack+MVVM+极限瘦身+极限启动优化+架构示例+全网唯一

(六) 大型项目架构:解析全动态插件化框架WXDynamicPlugin是如何做到全动态化的?
(七) 还在不断升级发版吗?从0到1带你看懂WXDynamicPlugin全动态插件化框架
(八) Compose插件化:一个Demo带你入门Compose,同时带你入门插件化开发
(九) 花式高阶:插件化之Dex文件的高阶用法,极少人知道的秘密
(十) Compose的全动态插件化框架支持了,已更新到AGP 8.6,Kotlin2.0.20,支持Compose
(十一)按需下载!!全动态插件化框架WXDynamicPlugin解析怎么支持的

一、前言

1、首先思考,一个大型Android项目前期准备哪些思考?

大型Android项目架构图3.png

2、基于上述思考架构之难点?

大型项目架构难度系数.jpeg

这里采用难度系数最大的 6颗星难度 来架构

3、这样的架构出发点是啥?

一切都是为了用户体验。

因为代码架构,最本质上,最初不应该只站在程序员架构的角度,而应该站在全视角角度的来看:

  1. 程序员的出现最本质,最初的目的是让用户复杂的操作简单化,换句话说:让用户体检简单流畅,复杂的东西留给程序员代码实现。
  2. 在时间和金钱允许的前提下:最大限度让用户的体验角度最好,包括用户无感知的全动态插件化,启动速度要最快,还要可以有很多功能,安装包的体积还要最小,稳定性要好。
  3. 架构的角度来讲:好的架构首先要实现功能,稳定性要好,其次是扩展性要好,代码接入要简单,解耦要彻底,再次是要符合编程设计规则等等。
  4. 从3中得出,实现功能和稳定性和前面的1和2都是为了让用户体验更好,而扩展性要好,代码接入要简单,解耦要彻底,还要符合编程设计规则等等这些都是为了方便开发者的。
  5. 如果即满足了客户体验最好,又满足了方便了开发者编程,这样的架构无疑是满分的。相反,以牺牲用户体验来满足开发者开发起来方便,那样只能算强行做到了:扩展性要好,代码接入简单,解耦彻底,符合编程设计规则,符合了某些设计模式,这样无疑是下下之策的架构。两者无法得兼,应该优先考虑到用户体验。否则,可以算作不合格。比如:架构解耦性很好,代码接入也简单,扩展性很好,但是你体积很大,或者影响到了启动速度。
  6. 很有可能某些架构设计:已经尽量最大努力了,包括一些中、大厂都是,功能一多,启动速度,全动态插件化,极度瘦身还是做不好,诚然,这是有点矛盾的,可以参考本篇文章后面介绍。
  7. 前面我的思维导图里面优化点那么多,为什么我总盯着,启动速度,全动态插件化,瘦身优化这三个上面,其实不难理解,这三个是矛盾的,很难实现,其他优化只要掌握了,就可以做了,和其他优化点没有冲突的。

基于上面思考后,决定搭建一套全动态插件化框架:WXDynamicPlugin,该框架研发设计断断续续耗时2年多,然后基于它再套嵌 Kotlin + 协程 + Flow + Retrofit + JetPack + MVVM + 视频播放 + 音乐播放,然后结合,极限瘦身、启动优化进行架构示例 ,当然后续会对其他优化进行补充,因为其他随便一个优化都可以单独出一篇了。该实例麻雀虽小,五脏俱全,肯定会给大家带来一些思考和启迪。最小的麻雀却包含了最基础的:

  1. 示例了全动态插件化框架WXDynamicPlugin、模块化、MVVM
  2. 示例了 Kotlin + 协程 + Flow + Retrofit + Okhttp 写法
  3. 示例了数据库 Room , 快速存储 MMKV 的用法
  4. 示例了 Exoplayer + MediaBrowser + 
    MediaBrowserService + MediaController+
    MediaSession
       + 套装多媒体 播放音乐
  5. 示例了 Exoplayer播放视频
  6. 示例了 全动态化插件化 框架中的各个组件写法
  7. 示例了 插件化式分包 极度瘦身
  8. 示例了 布局优化,极限启动优化 示例
  9. 示例了 深色模式 及 换肤功能
  10. 示例了 webview加载网页,如何解析网页,并修改网页中内容,及缓存网页
  11. 涉及到了 Protobuf ,Okio 等相关知识
  12. 涉及到了 注解处理器APT 的用法
  13. 涉及到了 gradle 的task 相关部分命令
  14. 涉及到了 ANT 编程相关命令

二、项目简介

  • 项目架构模式:采用自研全动态插件化框架WXDynamicPlugin进行插件化部署
  • 项目内部架构:采用 Kotlin 语言编写,架构选用 MVVM 代码架构模式,联合使用 Jetpack 相关控件:Room, Lifecyle,LiveData,ViewModel,等
  • 项目网络封装:采用协程 + Flow + Retrofit + OkHttp
  • 项目图片加载:采用谷歌开源Glide图片加载框架
  • 项目音乐视频:采用谷歌开源Exoplayer框架进行视频播放,音乐播放
  • 项目数据库:使用官方Jetpack中组件Room数据库
  • 项目键值对存储系统库:使用腾讯开源MMKV
  • 项目中网页解析修改:采用jsoup框架解析修改网页
  • 项目启动优化:采用谷歌官方startup启动框架+协程+预先布局+预先measure + layout极限提高首屏加载速度
  • 项目UI:采用MD设计,SwipeRefreshLayout刷新,ViewPager2 + TabLayout滑动翻页

项目插件中采用 MVVM架构模式,遵循 Google 推荐的架构,通过 Repository进行数据源的提供,ViewModel 联合生命周期进行数据内存上使用,数据加载应该全由 Repository 来完成:

CAp2mqSOIs.jpg

项目整体架构:

整体架构.png

项目截图:
零反射,零HooK,全动态化,插件化框架,全网唯一结合启动优化的插件化架构(二),
上一行文章有提到过,展示过,避免重复,在此不截图了

在此只截图几张看 过度绘制的图: 结果基本一遍白了

插件打包体积:总体插件打包体积 414k,最大单体插件模块也才 77K

img_v3_02bv_a9b87d9a-81a6-4d72-839f-7bf84674171g.jpg

宿主Host App 体积:9.37M

c0066da7-0a62-443b-b429-328c8d2f864b.jpeg

启动速度:平均:348ms (注:这是启动宿主app后,再此直接加载SD卡上插件后的启动速度)

hU5TYfMdQM.jpg

项目地址:
github地址
gitee地址

三、项目详情

3.1. 基础架构
(1)Base封装: 包括最基础的设置 BaseActivity:最基础的不分MVC,MVP,MVVM式封装:
只提供加载弹窗,toast,是否透明状态栏导航栏, 退出程序,回退键处理,Activity 界面第一帧开始绘制是初始化方法,前置初始化方法

Base架构.png

BaseActivity

abstract class BaseActivity : FragmentActivity() {
    private var loading: CommonLoadingView? = null
    private var initFlag = false
    open fun hasNavigationBarStatusBarTranslucent() = true

    override fun onCreate(savedInstanceState: Bundle?) {
        if (hasNavigationBarStatusBarTranslucent()) StatusBarUtil.setStatusBarTranslucent(this)
        beforeSuperOnCreate(savedInstanceState);
        super.onCreate(savedInstanceState)
        initX(savedInstanceState)
    }

    protected open fun initX(savedInstanceState: Bundle?) {
        initControl(savedInstanceState)
        bindEvent()
        initValue()
    }

    override fun onWindowFocusChanged(hasFocus: Boolean) {
        super.onWindowFocusChanged(hasFocus)
        if (hasFocus) {
            if (!initFlag) {
                initFlag = true
                lazyInitValue()
            }
        }
    }

    fun onToast(content: String) {
        CommonToast.show(content);
    }

    //是否loading
    open fun isShowloading(): Boolean? {
        return loading?.isShowing()
    }

    open fun showloading(showText: String?) {
        if (null == loading) loading = CommonLoadingView(this)
        if (isShowloading() == true) return
        if (showText != null) loading?.show(showText)
    }

    open fun hideLoading() {
        loading?.dismiss()
        loading = null
    }

    override fun onDestroy() {
        hideLoading()
        super.onDestroy()
    }

    override fun onBackPressed() {
        super.onBackPressed()
        finishActivity()
    }

    open fun beforeSuperOnCreate(savedInstanceState: Bundle?) {}
    open abstract fun initControl(savedInstanceState: Bundle?)
    open abstract fun bindEvent()
    open abstract fun initValue()
    open fun lazyInitValue() {}


    private var exitTime: Long = 0

    protected open fun exitApp() {
        if (System.currentTimeMillis() - exitTime > 2000) {
            onToast("再按一次退出程序")
            exitTime = System.currentTimeMillis()
        } else {
            WActivityManager.exitApplication()
        }
    }
}

BaseViewPluginResActivity

abstract class BaseViewPluginResActivity(protected val packageNamePlugin: String) : BaseActivity() {

    protected lateinit var resourcesPlugin: Resources

    override fun attachBaseContext(newBase: Context) {
        super.attachBaseContext(newBase)
        getPluginResources()?.let {
            resourcesPlugin = it
        }
    }

    override fun onStart() {
        super.onStart()
        onChangeSkin(getSkinResources())
    }

    abstract fun getSkinResources(): Resources

    abstract fun getPluginResources(): Resources?

    protected fun getPluginDrawable(resName: String): Drawable = ResourceUtils.getPluginDrawable(resourcesPlugin, resName, packageNamePlugin)

    protected fun getPluginDrawable(skinRes: Resources, resName: String, skinPackageName: String): Drawable = ResourceUtils.getPluginDrawable(skinRes, resName, skinPackageName)

    protected fun getPluginID(resName: String) = ResourceUtils.getPluginID(resourcesPlugin, resName, packageNamePlugin)

    protected fun getPluginID(skinRes: Resources, resName: String, skinPackageName: String) = ResourceUtils.getPluginID(skinRes, resName, skinPackageName)

    open fun onChangeSkin(skinRes: Resources) {

    }

    open fun callChangeSkin(skinRes: Resources) {
        onChangeSkin(skinRes)
        supportFragmentManager.fragments.forEach {
            if (it.isAdded && it is BasePluginResFragment) {
                it.callChangeSkin(skinRes)
            }
        }
    }
}

BaseViewModelActivity:

abstract class BaseViewModelActivity<VM : BaseViewModel>(packageNamePlugin: String) : BaseViewPluginResActivity(packageNamePlugin) {

    protected val viewModel by lazyViewModels()

    override fun bindEvent() {
        viewModel?.run {
            showUIDialog.observe(this@BaseViewModelActivity, Observer { it ->
                if (it.isShow) showloading(it.msg) else hideLoading()
            })
            errorMsgLiveData.observe(this@BaseViewModelActivity, Observer {
                onToast(it)
            })
        }
    }

    override fun initValue() {
    }


    @MainThread
    inline fun lazyViewModels(): Lazy<VM> {
        val cls = (javaClass.genericSuperclass as ParameterizedType).actualTypeArguments[0] as Class<VM>
        return ViewModelLazy(cls.kotlin, { viewModelStore }, { defaultViewModelProviderFactory })
    }
}

(2). BaseFragment 内设计和BaseActivity 设计基本一致
(3). BasePluginRecyclerAdapter 封装:

abstract class BasePluginRecyclerAdapter<T>(protected val re: Resources, private val packageName: String) : RecyclerView.Adapter<BasePluginRecyclerAdapter.BaseBindingViewHolder>() {

    var context: Context? = null
    protected lateinit var mData: MutableList<T>
    lateinit var skinRes: Resources

    fun notifyData(mData: MutableList<T>, skinRes: Resources) {
        if (mData == null) {
            this.mData = mutableListOf()
        } else {
            this.mData = mData
        }
        this.skinRes = skinRes
        notifyDataSetChanged()
    }

    fun notifySkinRes(skinRes: Resources) {
        this.skinRes = skinRes
        notifyDataSetChanged()
    }

    fun removeItem(position: Int) {
        mData?.takeIf {
            it.size > position
        }?.run {
            removeAt(position)
            notifyDataSetChanged()
        }
    }

    fun clearList() {
        mData?.run {
            clear()
            notifyDataSetChanged()
        }
    }

    override fun getItemCount(): Int = if (!this::mData.isInitialized) 0 else mData.size

    override fun getItemId(position: Int): Long {
        return position.toLong()
    }

    fun getItem(position: Int): T = mData[position]

    protected abstract fun getLayoutResIdName(viewType: Int): String

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseBindingViewHolder {
        if (context == null) {
            context = parent.context
        }
        val layoutID = re.getIdentifier(getLayoutResIdName(viewType), "layout", packageName)
        val xmlResourceParser = re.getLayout(layoutID)
        val view = LayoutInflater.from(context).inflate(xmlResourceParser, parent, false)
        return BaseBindingViewHolder(view)
    }

    override fun onBindViewHolder(holder: BaseBindingViewHolder, position: Int) {
        val item = getItem(position)
        onBindItem(context!!, item, holder, position)
    }

    fun <T : View> findViewByID(contentView: View, resourcesPlugin: Resources, IDResName: String): T {
        val ID = resourcesPlugin.getIdentifier(IDResName, "id", packageName)
        return contentView.findViewById(ID)
    }

    fun getPluginColorID(IDResName: String): Int {
        val ID = re.getIdentifier(IDResName, "color", packageName)
        return ID
    }

    fun getPluginColor(IDResName: String): Int {
        return re.getColor(getPluginColorID(IDResName))
    }

    fun getPluginColor(re: Resources, IDResName: String, packageName: String): Int {
        return re.getColor(getPluginColorID(re, IDResName, packageName))
    }

    fun getPluginColorID(re: Resources, IDResName: String, packageName: String): Int {
        val ID = re.getIdentifier(IDResName, "color", packageName)
        return ID
    }

    protected abstract fun onBindItem(context: Context, item: T, holder: RecyclerView.ViewHolder, position: Int)

    class BaseBindingViewHolder constructor(itemView: View) : RecyclerView.ViewHolder(itemView)
}

(4).BaseViewModel 封装:包含 toast提示文案: errorMsgLiveData,异步加载转圈:showUIDialog ,以及flow 流程里面结合 errorMsgLiveData,showUIDialog 使用的封装:

abstract class BaseViewModel : ViewModel() {
    val ViewModel.errorMsgLiveData by lazy { MutableLiveData<String>() }
    val ViewModel.showUIDialog by lazy { MutableLiveData<DialogBean>() }

    fun ViewModel.show(strMessage: String = "正在请求数据") {
        val showBean = showUIDialog.value ?: DialogBean(strMessage, true)
        showBean.isShow = true
        showBean.msg = strMessage
        showUIDialog.postValue(showBean)
    }

    fun ViewModel.hide() {
        val showBean = showUIDialog.value ?: DialogBean("", true)
        showBean.isShow = false
        showUIDialog.postValue(showBean)
    }

    override fun onCleared() {
        viewModelScope.cancel()
    }

    abstract fun start()

    protected fun <T> Flow<T>.flowOnIOAndCatch(): Flow<T> = flowOnIOAndCatch(errorMsgLiveData)

    protected fun <T> Flow<T>.onStartAndShow(strMessage: String = "正在请求数据"): Flow<T> = onStart {
        show()
    }

    protected fun <T> Flow<T>.onCompletionAndHide(): Flow<T> = onCompletion {
        hide()
    }

    protected suspend fun <T> Flow<T>.onStartShowAndFlowOnIOAndCatchAndOnCompletionAndHideAndCollect() {
        onStartAndShow().onCompletionAndHide().flowOnIOAndCatch().collect()//这里,开始结束全放在异步里面处理
    }

    fun <T> flowAsyncWorkOnViewModelScopeLaunch(flowAsyncWork: suspend () -> Flow<T>) {
        viewModelScope.launch {
            flowAsyncWork.invoke().onStartShowAndFlowOnIOAndCatchAndOnCompletionAndHideAndCollect()
        }
    }
}

(5).扩展函数类:

  1. Context.kt: 扩展了单位转化等
  2. Fragment.kt: 扩展了Fragment 切换fragment 显示 隐藏等
  3. FragmentActivity.kt: 扩展了Activity中 切换fragment 显示隐藏 及其他相关等功能
  4. ImageView.kt: 扩展了Glide 直接加载图片等
  5. FLow.KTL 扩展了catch 异常处理,并对10多种网络异常进行对应的文字提示:
<string name="ElseNetException">未知错误异常</string>
<string name="SocketTimeoutException">网络超时</string>
<string name="TimeoutException">网络超时</string>
<string name="SocketException">网络错误</string>
<string name="ConnectException">网络错误</string>
<string name="HttpException">服务器异常</string>
<string name="UnknownHostException">无网络连接</string>
<string name="HostBaseUrlError">请求地址错误</string>
<string name="Mobilenetuseless_msg">当前网络不可用</string>
<string name="failed_to_connect_to">无法连接到服务器</string>
<string name="JsonSyntaxException">数据错误,json解析错误</string>

3.2. JetPack和material相关组件
(1). BottomNavigationView: 项目主页菜单栏使用了该组件 ,包含了 4个菜单 :首页 ,收藏,示例,设置

(2). ViewPage2 + TabLalyout: 首页Tab使用了该组件,tab 包含了:手机,新闻,娱乐,体育,时尚,财经,汽车,军事,科技

(3).ViewModel: 具备生命感知能力的数据存储组件。页面配置更改数据不会丢失,数据共享(单 Activity 多 Fragment 场景下的数据共享),以生命周期的方式管理界面相关的数据,通常和 DataBinding 配合使用,为实现 MVVM 架构提供了强有力的支持。

(4).Lifecycle: 生命周期感知型组件可执行操作来响应另一个组件(如 activity 和 fragment)的生命周期状态的变化。这些组件有助于您写出更有条理且往往更精简的代码,这样的代码更易于维护

(5).LiveData: 是一种具有生命周期感知能力、可观察的数据存储器类。

(6).Room: Room 持久性库在 SQLite 的基础上提供了一个抽象层,让用户能够在充分利用 SQLite 的强大功能的同时,获享更强健的数据库访问机制

本示例中 为了示例Room 只做了收藏图片功能, 长按首页 列表,即可收藏到收藏列表,长按收藏列表即删除收藏了。
Room包含三件套:dao, db, table
dao 负责数据库的操作 增删查改
db 负责保存数据库并作为应用持久性数据底层连接的主要访问点
table 数据库里面的表名,可以多个

数据库:


@Database(entities = [CollectTableBean::class], version = 1, exportSchema = false)
abstract class CollectDataBase : RoomDatabase() {

    companion object {
        @Volatile
        private var instance: CollectDataBase? = null
        fun getInstance(context: Context, roomDBMigration: RoomDBMigration): CollectDataBase {
            if (instance == null) {
                synchronized(CollectDataBase::class.java) {
                    if (instance == null) {
                        val builder = Room.databaseBuilder(context, CollectDataBase::class.java, "wx_sample_db")
                        val migrations = roomDBMigration.createMigration()
                        migrations?.takeIf {
                            it.isNotEmpty()
                        }?.let {
                            builder.addMigrations(*it)
                        }
                        builder.addCallback(object : RoomDatabase.Callback() {
                            override fun onCreate(db: SupportSQLiteDatabase) {
                                super.onCreate(db)
                                WLog.e(this@Companion, "RoomDatabase onCreate")
                            }

                            override fun onOpen(db: SupportSQLiteDatabase) {
                                super.onOpen(db)
                                WLog.e(this@Companion, "RoomDatabase onOpen")
                            }

                            override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
                                super.onDestructiveMigration(db)
                                WLog.e(this@Companion, "RoomDatabase onDestructiveMigration")
                            }
                        })
                        instance = builder.build()
                    }
                }
            }
            return instance!!
        }
    }

    abstract fun collectDao(): CollectDao
}

数据收藏表:

@Entity(tableName = "collect_tab")
class CollectTableBean(
    @PrimaryKey @ColumnInfo(name = "id", typeAffinity = ColumnInfo.INTEGER, defaultValue = "0") var id: Long,
    @ColumnInfo(name = "title", typeAffinity = ColumnInfo.TEXT, defaultValue = "") val title: String,
    @ColumnInfo(name = "imgUrl", typeAffinity = ColumnInfo.TEXT, defaultValue = "") val imgUrl: String,
    @ColumnInfo(name = "createTime", typeAffinity = ColumnInfo.INTEGER, defaultValue = "") val createTime: Long
)

数据操作dao:

@Dao
interface CollectDao {

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun insertCollectBean(collectTableBean: CollectTableBean)

    //ASC 默认值,从小到大,升序排列 DESC 从大到小,降序排列
    @Query("SELECT * FROM collect_tab ORDER BY createTime ASC")
    fun getList(): LiveData<MutableList<CollectTableBean>>

    @Query("SELECT COUNT(*) FROM collect_tab WHERE id = :uuID")
    fun queryByUUID(uuID: Long): Int

    @Query("DELETE FROM collect_tab WHERE id =:id")
    fun deleteFromID(id: Long)
}

3.3、网络封装:
网络封装采用 Retrofit + Okhttp + 协程 + Flow

在Repository中

suspend fun getNetTabInfo(path: String, start: Int, end: Int) = flow {
    val str = apiL.getNetTabInfo(path, start.toString(), end.toString(), start).let {
        it.substring(9, it.length - 1)
    }
    val map = Gson().fromJson<MutableMap<String, MutableList<NewsBean>>>(str, object : TypeToken<Map<String, MutableList<NewsBean>>>() {}.type)
    emit(map[path]!!)
}

在viewModel中只需要调用:flowAsyncWorkOnViewModelScopeLaunch,请求时,请求结束,loading框显示和消失都已经在前面BaseViewModel中介绍过,网络错误异常也在前面Flow.kt 扩展方法中统一处理过了。

fun getData(key: String) {
    isLoadingMore[key] = true
    isClick = false
    flowAsyncWorkOnViewModelScopeLaunch {
        val start = pageNoMap[key]!!
        val end = start + 10
        newsRepositoryL.getNetTabInfo(key, start, end).onEach {
            if (!isLoadOffine)
                if (pageNoMap[key] == 0) {
                    WLog.e(this@HomeTabViewModel, key)
                    result[key]?.postValue(it)
                } else {
                    val list = result[key]?.value
                    list?.removeAt(list.size - 1)
                    list?.addAll(it)
                    result[key]?.postValue(list)
                }
            else isLoadOffine = false
            var c = liveDataLoadSuccessCount.value?.plus(1)
            liveDataLoadSuccessCount.postValue(c)
            enableLoadeMore[key]?.postValue(it.size == 10)
            if (it.size == 10)
                pageNoMap[key] = pageNoMap[key]!!.plus(10)
            isLoadingMore[key] = false
        }
    }
}

3.4、图片加载封装:
直接在ImageView扩展了Glide 加载图片方法

fun ImageView.loadUrl(url: String) {
       Glide.with(context).load(url)
        .skipMemoryCache(false) 
        .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
        .into(this)
}

3.5、MMKV使用:

MMKV 是基于 mmap 内存映射的 key-value 组件,底层序列化/反序列化使用 protobuf 实现,性能高,稳定性强 初始化写在了启动框架startup里面。

object InitHomeFirstInitializeHelp {

    fun initCreate(context: Context) {
        CoroutineScope(Dispatchers.IO).launch {
            ScreenManager.initScreenSize(context)
            MMKV.initialize(context)  //初始化MMKV

3.6、音乐播放: 采用谷歌开源ExoPlayer播放

联合使用Exoplayer + MediaBrowser + MediaBrowserService + MediaController + MediaSession 套装多媒体 播放音乐,并结合 Notification 结合 前台service 保持音乐长时间在后台播放

这部分代码太多只贴部分了:

private val uAmpAudioAttributes by lazy {
    AudioAttributes.Builder()
        .setContentType(C.AUDIO_CONTENT_TYPE_MUSIC)
        .setUsage(C.USAGE_MEDIA)
        .build()
}

val exoPlayer: ExoPlayer by lazy {
    ExoPlayer.Builder(this).build().apply {
        setAudioAttributes(uAmpAudioAttributes, true)
        setHandleAudioBecomingNoisy(true)
        addListener(playerListener)
    }
}

val mediaSession by lazy {
    MediaSessionCompat(this, getString(R.string.app_name))
        .apply {
            setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS)
            val pendingFlags = if (SdkIntUtils.isLollipop()) {
                PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
            } else {
                PendingIntent.FLAG_UPDATE_CURRENT
            }
            val sessionIntent = Intent(this@MusicService, NotificationTargetActivity::class.java)
            val sessionActivityPendingIntent = PendingIntent.getActivity(this@MusicService, 0, sessionIntent, pendingFlags)
            setSessionActivity(sessionActivityPendingIntent)
            isActive = true
        }
}

private val mediaSessionConnector by lazy {
    MediaSessionConnector(mediaSession).apply {
        setPlaybackPreparer(this@MusicService)
        setQueueNavigator(SSQueueNavigator(mediaSession))
    }
}

override fun onCreate() {
    super.onCreate()
    sessionToken = mediaSession.sessionToken
    notificationManager = WXPlayerNotificationManager(this, mediaSession, PlayerNotificationListener())
    notificationManager.showNotificationForPlayer(exoPlayer)
    mediaSessionConnector.setPlayer(exoPlayer)
    exoPlayer.clearMediaItems()
}

播放时前台service结合notification

/**
 * Listen for notification events.
 */
private inner class PlayerNotificationListener : WXNotificationListener {
    override fun onNotificationPosted(notificationId: Int, notification: Notification, ongoing: Boolean) {
        if (ongoing && !isForegroundService) {
            //启动前台服务
            ContextCompat.startForegroundService(this@MusicService, Intent(this@MusicService, this@MusicService.javaClass))
            startForeground(notificationId, notification)
            isForegroundService = true
        }
    }

    override fun onNotificationCancelled(notificationId: Int, dismissedByUser: Boolean) {
        stopForeground(true)
        isForegroundService = false
        stopSelf()
    }
}

3.7、视频播放: 采用谷歌开源ExoPlayer播放器播放视频

private fun buildMediaSource(uri: Uri): MediaSource {
    val dataSourceFactory = DefaultDataSourceFactory(activity)
    return ProgressiveMediaSource.Factory(dataSourceFactory).createMediaSource(MediaItem.fromUri(uri))
}


private fun initializePlayer(): Boolean {
    if (player == null) {
        val playerBuilder = ExoPlayer.Builder(activity)
            .setMediaSourceFactory(DefaultMediaSourceFactory(activity))
            .setRenderersFactory(DefaultRenderersFactory(activity))
            .setLoadControl(DefaultLoadControl())
        player = playerBuilder.build()
        player?.setAudioAttributes(AudioAttributes.DEFAULT, true)
        player?.playWhenReady = true
        playerView.player = player
    }
    val playUri: Uri = Uri.parse(url)
    val mediaSource: MediaSource = buildMediaSource(playUri)
    player?.prepare(mediaSource, true, false)
    player?.seekTo(startPosition)
    return true
}

3.8、解析Html,修改Html: 采用谷歌开源jsoup解析修改网页,先下载到本地,再修改,保存文件到本地IO数据流采用Okio 方式

val dir = "down_dir"
val file = File(StringBuilder(context.filesDir.absolutePath).append(File.separator).append(dir).append(File.separator).append(fileName).toString())
if (!file.exists()) {
    File(file.parent).takeUnless { it.exists() }?.run { mkdirs() }
    val html = apiL.getNewsDetailInfo("https://3g.163.com/all/article/${id}.html#offset=1")
    val document = Jsoup.parse(html, "https://3g.163.com/")
    document.select("script").remove()
    document.select(".main-openApp").remove()
    document.select(".operate").remove()
    document.select(".js-open-app").remove()
    document.select(".comment").remove()
    document.select(".recommend").remove()
    document.select("head")?.append("<script type="text/javascript" src="../js/jquery.js"></script>")
    document.select("head")?.append("<script type="text/javascript" src="../js/jquery.lazyload.js"></script>")
    document.select("head")?.append("<script> function loadImage(){setTimeout(function (){$("img.lazy").lazyload();window.scrollTo('0','1');window.scrollTo('1','0');}, 300);}</script>")

    val lint = document.select("link")
    lint?.forEach {
        val link = it?.attr("abs:href")
        if (link != null && link!!.contains("0ccc5aad.js")) {
            it.remove()
        }
    }

    val figure = document.select("figure")
    figure?.forEach {
        it.select("div")?.takeIf { d ->
            d.size > 1
        }?.get(1)?.remove()
    }
    val nav = document.select("header")
    nav?.remove()

    val newHtml = document.html()
        .replace("image-lazy image-error", "lazy")
        .replace("data-src", "data-original")
        .replace("image-lazy image-reload", "lazy")

    val inputStream = newHtml.byteInputStream()
    if (inputStream != null) {
        val sinkBuffer = file.sink().buffer()
        val bufferedSource = inputStream.source().buffer()
        sinkBuffer.write(bufferedSource.readByteArray())
        sinkBuffer.close()
        bufferedSource.close()
        inputStream.close()
    }
}
emit(StringBuilder().append("file:///").append(file.absolutePath).toString())

3.9、WebView加载网页: 先将长期固定不动的js,css放入本地assets下,然后让 webview 加载时拦截优先加载本地js和css

@RequiresApi(Build.VERSION_CODES.N)
override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest?): WebResourceResponse? {
    val url = request?.url.toString()
    val lastSlash: Int = url.lastIndexOf("/")
    if (lastSlash != -1) {
        val suffix: String = url.substring(lastSlash + 1)
        if (suffix.endsWith(".css")) {
            if (strOfflineResources.contains(suffix)) {
                val mimeType = "text/css"
                val offlineRes = "css/"
                val inputs = webResAssets.open("$offlineRes$suffix")
                return WebResourceResponse(mimeType, "UTF-8", inputs)
            } else {
                android.util.Log.e("ImplWebViewClient", "request css :${url}")
            }
        }
        if (suffix.endsWith(".js")) {
            if (strOfflineResources.contains(suffix)) {
                val mimeType = "application/x-javascript"
                val offlineRes = "js/"
                val inputs = webResAssets.open("$offlineRes$suffix")
                return WebResourceResponse(mimeType, "UTF-8", inputs)
            } else {
                android.util.Log.e("ImplWebViewClient", "request js :${url}")
            }
        }
    }
    return super.shouldInterceptRequest(view, request)
}

3.10、启动化框架: 采用谷歌官方 startup 组件 在启动框架中,主要做了 MMKV初始化,对首页首屏展示的UI布局进行了协程下的提前创建,layout, measure,到了首页直接拿到渲染

class InitHomeFirstInitialize : Initializer<Unit> {

    override fun create(activity: Context) {
        InitHomeFirstInitializeHelp.initCreate(activity)
    }

    override fun dependencies(): List<Class<out Initializer<*>>> {
        return emptyList()
    }
}
fun initCreate(context: Context) {
    CoroutineScope(Dispatchers.IO).launch {
        ScreenManager.initScreenSize(context)
        MMKV.initialize(context)

        val themeID = context.resources.getIdentifier("Theme.WXDynamicPlugin", "style", context.packageName)
        val context: Context = MutableContextWrapper(context.toTheme(themeID))
        val res = PluginResource.getSkinResources()
        HomeContains.putViewByKey(LaunchInflateKey.home_activity, async {
            GenerateHomeLayout.syncCreateHomeActivityLayout(context, res)
        })
        HomeContains.putViewByKey(LaunchInflateKey.home_navigation, async {
            GenerateHomeLayout.syncCreateHomeNavigationLayout(context, res)
        })
        HomeContains.putFragmentByKey(LaunchInflateKey.home_tab_fragment, async {
            HomeTabFragment()
        })
        HomeContains.putViewByKey(LaunchInflateKey.home_tab_fragment_layout, async {
            GenerateHomeLayout.syncCreateHomeTabFragmentLayout(context, res)
        })
        HomeContains.putViewByKey(LaunchInflateKey.home_fragment, async {
            GenerateHomeLayout.syncCreateHomeFragmentLayout(context, res)
        })
        async(Dispatchers.IO) {
            val am = PluginResource.getWebRes().assets
            try {
                val resJs = am.list("js")
                val strOfflineResources = StringBuilder()
                if (resJs != null && resJs.isNotEmpty()) {
                    for (i in resJs.indices) {
                        strOfflineResources.append(resJs[i])
                    }
                }
                val resCss = am.list("css")
                if (resCss != null && resCss.isNotEmpty()) {
                    for (i in resCss.indices) {
                        strOfflineResources.append(resCss[i])
                    }
                }
                MMKVHelp.saveJsPath(strOfflineResources.toString())
            } catch (e: Exception) {
                e.printStackTrace()
            }
        }

        /** 添加 ContentProvider **/
        WXProviderManager.instance.addContentProvider(TestContentProvider(context))
    }
}

四、全动态插件化

全动态插件化: 采用 WXDynamicPlugin,是由本人自主研发,详细请见篇头4篇文章,4篇文章介绍包含了 开发背景, 主要介绍,详细说明,接入指南

1. 主要目的是为了宿主只有空壳子app,宿主没有任何业务代码
2. 同时保证可以做到极度瘦身
3. 同时在做启动优化时,可以将启动速度做到极致
4. 以后升级可以无感知,修改bug无感知无需发版
5. 插件打包可以“分布式”,可以无感知只更新某一小块
6. 插件化框架无需考虑android 版本兼容性
7. 插件化框架sdk修改也动态更新,无需更新宿主
8. 插件化调试模式,插件化下载逻辑,部署服务器配置全部动态
9. 插件化调试支持打断点Debug,无需担心调试烦恼

五、总结

通过本项目介绍,你将会了解、理解如下些点:

  1. 深入理解大型全动态插件化框架的设计背景及出发点和难度:WXDynamicPlugin又是怎么做到: 既要做到全动态,还要插件化,还要追求启动速度极限快,还要在瘦身优化上做到极致。
  2. 理解了 Kotlin + 协程 + Flow + Retrofit + Okhttp 写法
  3. 理解了 数据库 Room , 快速存储 MMKV 的用法
  4. 理解了 Exoplayer + MediaBrowser + MediaBrowserService  + MediaController+ MediaSession 套装多媒体 播放音乐
  5. 理解了 Exoplayer播放视频
  6. 理解了 全动态化插件化 框架中的各个组件写法
  7. 理解了 插件化式分包 极度瘦身
  8. 理解了 布局优化,极限启动优化 示例
  9. 理解了 深色模式 及 换肤功能
  10. 理解了 webview加载网页,如何解析网页,并修改网页中内容,及缓存网页
  11. 还可以学习到 Protobuf ,Okio 等相关知识
  12. 还能再项目中 学习到 注解处理器APT 的用法
  13. 还可以学习到 gradle 的task 相关部分命令
  14. 还可以学习到 ANT 编程相关命令

感谢阅读:

点关注,不迷路,欢迎点赞