前言:十年寒窗磨一剑,六月沙场试锋芒,吃得苦中苦,方为人上人
(一)零反射,零HooK,全动态化,插件化框架,全网唯一结合启动优化的插件化架构
(二)零反射,零HooK,全动态化,插件化框架,全网唯一结合启动优化的插件化架构
(三)零反射,零HooK,全动态化,插件化框架,全网唯一结合启动优化的插件化架构
(四)零反射,零HooK,全动态化,插件化框架,全网唯一结合启动优化的插件化架构(五) 大型项目架构:全动态插件化+模块化+Kotlin+协程+Flow+Retrofit+JetPack+MVVM+极限瘦身+极限启动优化+架构示例+全网唯一
(六) 大型项目架构:解析全动态插件化框架WXDynamicPlugin是如何做到全动态化的?
(七) 还在不断升级发版吗?从0到1带你看懂WXDynamicPlugin全动态插件化框架
(八) Compose插件化:一个Demo带你入门Compose,同时带你入门插件化开发
(九) 花式高阶:插件化之Dex文件的高阶用法,极少人知道的秘密
一、前言
1、首先思考,一个大型Android项目前期准备哪些思考?
2、基于上述思考架构之难点?
这里采用难度系数最大的 6颗星难度
来架构
3、这样的架构出发点是啥?
一切都是为了用户体验
。
因为代码架构,最本质上,最初不应该只站在程序员架构的角度,而应该站在全视角角度的来看:
- 程序员的出现最本质,最初的目的是让用户复杂的操作简单化,换句话说:让用户体检简单流畅,复杂的东西留给程序员代码实现。
- 在时间和金钱允许的前提下:最大限度让用户的体验角度最好,包括用户无感知的全动态插件化,启动速度要最快,还要可以有很多功能,安装包的体积还要最小,稳定性要好。
- 架构的角度来讲:好的架构首先要实现功能,稳定性要好,其次是扩展性要好,代码接入要简单,解耦要彻底,再次是要符合编程设计规则等等。
- 从3中得出,实现功能和稳定性和前面的1和2都是为了让用户体验更好,而扩展性要好,代码接入要简单,解耦要彻底,还要符合编程设计规则等等这些都是为了方便开发者的。
- 如果即满足了客户体验最好,又满足了方便了开发者编程,这样的架构无疑是满分的。相反,以牺牲用户体验来满足开发者开发起来方便,那样只能算强行做到了:扩展性要好,代码接入简单,解耦彻底,符合编程设计规则,符合了某些设计模式,这样无疑是下下之策的架构。两者无法得兼,应该优先考虑到用户体验。否则,可以算作不合格。比如:架构解耦性很好,代码接入也简单,扩展性很好,但是你体积很大,或者影响到了启动速度。
- 很有可能某些架构设计:已经尽量最大努力了,包括一些中、大厂都是,功能一多,启动速度,全动态插件化,极度瘦身还是做不好,诚然,这是有点矛盾的,可以参考本篇文章后面介绍。
- 前面我的思维导图里面优化点那么多,为什么我总盯着,启动速度,全动态插件化,瘦身优化这三个上面,其实不难理解,这三个是矛盾的,很难实现,其他优化只要掌握了,就可以做了,和其他优化点没有冲突的。
基于上面思考后,决定搭建一套全动态插件化框架:WXDynamicPlugin,该框架研发设计断断续续耗时2年多,然后基于它再套嵌 Kotlin
+ 协程 + Flow
+ Retrofit
+ JetPack
+ MVVM
+ 视频播放 + 音乐播放,然后结合,极限瘦身、启动优化进行架构示例 ,当然后续会对其他优化进行补充,因为其他随便一个优化都可以单独出一篇了。该实例麻雀虽小,五脏俱全,肯定会给大家带来一些思考和启迪。最小的麻雀却包含了最基础的:
- 示例了全动态插件化框架WXDynamicPlugin、模块化、MVVM
- 示例了
Kotlin + 协程 + Flow + Retrofit + Okhttp
写法 - 示例了数据库
Room
, 快速存储MMKV
的用法 - 示例了
Exoplayer
+MediaBrowser
+
MediaBrowserService
+MediaController
+
MediaSession
+ 套装多媒体 播放音乐 - 示例了
Exoplayer
播放视频 - 示例了
全动态化插件化
框架中的各个组件写法 - 示例了
插件化式分包 极度瘦身
- 示例了
布局优化,极限启动优化
示例 - 示例了
深色模式
及换肤功能
- 示例了
webview加载网页,如何解析网页,并修改网页中内容,及缓存网页
- 涉及到了
Protobuf ,Okio
等相关知识 - 涉及到了
注解处理器APT
的用法 - 涉及到了
gradle 的task
相关部分命令 - 涉及到了
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
来完成:
项目整体架构:
项目截图:
零反射,零HooK,全动态化,插件化框架,全网唯一结合启动优化的插件化架构(二),
上一行文章有提到过,展示过,避免重复,在此不截图了
在此只截图几张看 过度绘制
的图: 结果基本一遍白了
插件打包体积:总体插件打包体积 414k
,最大单体插件模块也才 77K
宿主Host App 体积:9.37M
启动速度:平均:348ms
(注:这是启动宿主app后,再此直接加载SD卡上插件后的启动速度)
三、项目详情
3.1. 基础架构
(1)Base封装: 包括最基础的设置
BaseActivity
:最基础的不分MVC,MVP,MVVM式封装:
只提供加载弹窗,toast,是否透明状态栏导航栏
, 退出程序,回退键处理
,Activity 界面第一帧开始绘制是初始化方法,前置初始化方法
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).扩展函数类:
- Context.kt: 扩展了单位转化等
- Fragment.kt: 扩展了Fragment 切换fragment 显示 隐藏等
- FragmentActivity.kt: 扩展了Activity中 切换fragment 显示隐藏 及其他相关等功能
- ImageView.kt: 扩展了Glide 直接加载图片等
- 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,无需担心调试烦恼
五、总结
通过本项目介绍,你将会了解、理解如下些点:
- 深入理解大型全动态插件化框架的设计背景及出发点和难度:WXDynamicPlugin又是怎么做到: 既要做到全动态,还要插件化,还要追求启动速度极限快,还要在瘦身优化上做到极致。
- 理解了
Kotlin + 协程 + Flow + Retrofit + Okhttp
写法 - 理解了 数据库
Room
, 快速存储MMKV
的用法 - 理解了
Exoplayer
+MediaBrowser
+MediaBrowserService
+MediaController
+MediaSession
套装多媒体 播放音乐 - 理解了
Exoplayer
播放视频 - 理解了
全动态化插件化
框架中的各个组件写法 - 理解了
插件化式分包 极度瘦身
- 理解了
布局优化,极限启动优化
示例 - 理解了
深色模式
及换肤功能
- 理解了
webview加载网页,如何解析网页,并修改网页中内容,及缓存网页
- 还可以学习到
Protobuf ,Okio
等相关知识 - 还能再项目中 学习到
注解处理器APT
的用法 - 还可以学习到
gradle 的task
相关部分命令 - 还可以学习到
ANT
编程相关命令