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

12,664 阅读16分钟

1213.webp

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

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

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

(六) 大型项目架构:解析全动态插件化框架WXDynamicPlugin是如何做到全动态化的?
(七) 还在不断升级发版吗?从0到1带你看懂WXDynamicPlugin全动态插件化框架
(八) Compose插件化:一个Demo带你入门Compose,同时带你入门插件化开发
(九) 花式高阶:插件化之Dex文件的高阶用法,极少人知道的秘密

一、前言

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 相关控件:RoomLifecyleLiveDataViewModel,等
  • 项目网络封装:采用协程 + 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. JetPackmaterial相关组件
(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 加载时拦截优先加载本地jscss

@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  + MediaControllerMediaSession 套装多媒体 播放音乐
  5. 理解了 Exoplayer播放视频
  6. 理解了 全动态化插件化 框架中的各个组件写法
  7. 理解了 插件化式分包 极度瘦身
  8. 理解了 布局优化,极限启动优化 示例
  9. 理解了 深色模式换肤功能
  10. 理解了 webview加载网页,如何解析网页,并修改网页中内容,及缓存网页
  11. 还可以学习到 Protobuf ,Okio 等相关知识
  12. 还能再项目中 学习到 注解处理器APT 的用法
  13. 还可以学习到 gradle 的task 相关部分命令
  14. 还可以学习到 ANT 编程相关命令

感谢阅读:

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