0%

近期实现的一个功能中,由于后台需要的请求入参数据量过大,而req的载体有大小限制,所以采用了先使用pb记录数据存储上云,云文件索引作为req入参的方案。
本文记录其中使用pb与Java互转的过程。

前言

protobuf是谷歌推出的序列化协议,有比json所占字节少体积小、序列化传输快的特点。再加上其同样有平台无关的特性,protobuf得以广泛应用。

常见的使用场景:

  1. 前端与后端的通信数据载体
  2. 多端复用的数据结构,如草稿库
  3. 作为轻量级文件存储数据(本次的使用场景)

ProtoBuf转成Java过程记录

编写proto文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
syntax = "proto2";  

package demo;

option java_multiple_files = true;
option java_package = "com.darrenyuen.demo.protos";
option java_outer_classname = "DemoProtos";

message Person {
optional string name = 1;
optional int32 id = 2;
optional string email = 3;

enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}

message PhoneNumber {
optional string number = 1;
optional PhoneType type = 2 [default = HOME];
}

repeated PhoneNumber phones = 4;
}

message AddressBook {
repeated Person people = 1;
}

命令编译

proto语法是跨语言的,所以我们需要需要对应平台的编译器工具,编译成java文件,比如笔者在MacOs下,需要下载这个平台的编译工具。

1
protoc --java_out=输出目录 编译文件 -I=编译文件所在的文件夹
  • protoc:Protobuf 编译器的命令行工具,用于编译 Protobuf 文件。
  • --java_out=输出目录:指定生成的 Java 代码的输出目录。编译器会将生成的代码放置在这个目录下。
  • 编译文件:Protobuf 文件的路径,这些文件包含了消息定义。
  • -I=编译文件所在的文件夹:指定 Protobuf 文件所在的目录。编译器会在这个目录下搜索指定的文件。
    执行编译后,便可拷贝生成java文件到工程中使用

集成插件编译

  1. 集成gradle插件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 根目录 build.gradle文件,引入插件
    buildscript {
    repositories {
    mavenCentral()
    }

    dependencies {
    classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.18'
    }
    }
    ``
    1
    2
    // app目录 build.gradle 引入pb-java库
    implementation 'com.google.protobuf:protobuf-java:3.8.0'
1
2
3
4
// app目录 build.gradle 声明使用插件
plugins {
id 'com.google.protobuf'
}
  1. 编译配置
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // app目录 build.gradle
    sourceSets {
    main {
    proto {
    srcDir 'src/main/proto'
    }
    }
    }

    protobuf {
    protoc {
    artifact = 'com.google.protobuf:protoc:3.18.1'
    }
    generateProtoTasks {
    all().each { task ->
    task.builtins {
    java {
    option "lite"
    }
    }
    }
    }
    }
  2. 使用结果文件
    编译后,结果文件会生成并存储在{PROJECT_ROOT}/app/build/generated/source/proto中,可拷贝到工程中使用

Java数据序列化成pb文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
val filePath = "dir/${System.currentTimeMillis()}.pb"  
var outputStream: FileOutputStream? = null
try {
val demoParam = addressBookBuilder.build()
FileUtils.createOrExistsFile(File(filePath))
outputStream = FileOutputStream(filePath)
try {
demoParam.writeTo(outputStream)
} catch (t: Throwable) {

}
} catch (t: Throwable) {

} finally {
outputStream?.close()
}

(一)OpenGL函数库
格式:

<库前缀><根命令><可选的参数个数><可选的参数类型> 库前缀有 gl、glu、aux、glut、wgl、glx、agl 等等。

1、核心函数库主要可以分为以下几类函数
(1) 绘制基本的几何图元函数。如:glBegain().

(2) 矩阵操作、几何变换和投影变换的函数。如:矩阵入栈glPushMatrix(),还有矩阵的出栈、转载、相乘,此外还有几何变换函数glTranslate*(),投影变换函数glOrtho()和视口变换函数glViewport()等等。

(3) 颜色、光照和材质函数。

(4) 显示列表函数,主要有创建、结束、生成、删除和调用显示列表的函数glNewList()、glEndList()、glGenLists()、glDeleteLists()和glCallList()。

(5) 纹理映射函数,主要有一维和二维纹理函数,设置纹理参数、纹理环境和纹理坐标的函数glTexParameter*()、glTexEnv*()和glTetCoord*()等。

(6) 特殊效果函数。

(7) 选着和反馈函数。

(8) 曲线与曲面的绘制函数。

(9) 状态设置与查询函数。

(10) 光栅化、像素函数。

2、OpenGL实用库(The OpenGL Utility Library)(GLU)
包含有43个函数,函数名的前缀名为glu.

(1) 辅助纹理贴图函数。

(2) 坐标转换和投影变换函数。

(3) 多边形镶嵌工具。

(4) 二次曲面绘制工具。

(5) 非均匀有理B样条绘制工具。

(6) 错误反馈工具,获取出错信息的字符串gluErrorString()

3、OpenGL辅助库(AUX)
包含有31个函数,函数名前缀名为aux

这部分函数提供窗口管理、输入输出处理以及绘制一些简单的三维物体。

4、OpenGL工具库(OpenGL Utility Toolkit)(GLUT)
包含大约30多个函数,函数前缀名为glut,此函数由glut.dll来负责解释执行。

(1) 窗口操作函数。窗口初始化、窗口大小、窗口位置等函数glutInit() glutInitDisplayMode()、glutInitWindowSize() glutInitWindowPosition()等。

(2) 回调函数。响应刷新消息、键盘消息、鼠标消息、定时器函数等,GlutDisplayFunc()、glutPostRedisplay()、 glutReshapeFunc()、glutTimerFunc()、glutKeyboardFunc()、 glutMouseFunc()。

(3) 创建复杂的三维物体。这些和aux库函数功能相同。如创建球体glutWireSphere().

(4) 函数菜单

(5) 程序运行函数 glutAttachMenu()

5、16个WGL函数,专门用于OpenGL和Windows窗口系统的联接,其前缀名为wgl
(1) 绘制上下文函数。 wglCreateContext()、wglDeleteContext()、wglGetCurrentContent()、wglGetCurrentDC() wglDeleteContent()等。

(2) 文字和文本处理函数。wglUseFontBitmaps()、wglUseFontOutlines()。

(3) 覆盖层、地层和主平面处理函数。wglCopyContext()、wglCreateLayerPlane()、 wglDescribeLayerPlane()、wglReakizeLayerPlatte()等。

(4) 其他函数。wglShareLists()、wglGetProcAddress()等。

(二)函数库列表
1、OpenGL应用函数库
gluBeginCurve,gluEndCurve 定义一条不一至的有理的NURBS曲线
gluBeginPolygon,gluEndPolygon 定义一个非凸多边形
gluBeginSurface,gluEndSurface 定义一个NURBS曲线
gluBeginTrim,gluEndTrim 定义一个NURBS整理循环
gluBuild1Dmipmaps 建立一维多重映射
gluBuild2Dmipmaps 建立二维多重映射
gluCylinder 绘制一个圆柱
gluDeleteNurbsRenderer 删除一个NURBS对象
gluDeleQuadric 删除一个二次曲面对象
gluDeleteTess 删除一个镶嵌对象
gluDisk 绘制一个盘子
gluErrorString 根据OpenGL或GLU错误代码产生错误字符串
gluGetNutbsProperty 得到一个NURBS属性
gluGetString 得到一个描述GLU版本号或支持GLU扩展调用的字符串
gluGetTessProperty 得到一个镶嵌对象
gluLoadSamplingMatrices 加载NUMRBS例子和精选矩阵
gluLookAt 设定一个变换视点
gluNewNurbsRenderer 创建一个NURBS对象
gluNewQuadric 建立一个二次曲面对象
gluNewTess 建立一个镶嵌对象
gluNextContour 为其他轮廓的开始做标记
gluNurbsCallback 为NURBS对象设定一个回调
gluNnrbsCurve 设定一个NuRBS曲线的形状
gluNurbsProperty 设定一个NURBS属性
gluNurbsSurface 定义一个NURBS表面的形状
gluOrtho2D 定义一个二位正交投影矩阵
gluPartialDisk 绘制一个盘子的弧
gluPerspective 设置一个透视投影矩阵
gluPickMatrix 定义一个拾取区间
gluProject 将对象坐标映射为窗口坐标
gluPwlCurve 描述一个分段线性NURBS修剪曲线
gluQuadricCallback 为二次曲面对象定义一个回调
gluQuadricDrawStyle 为二次曲面设定合适的绘制风格
gluQuadricNormals 定义二次曲面所用的法向的种类
gluQuadricOrientation 定义二次曲面内部或外部方向
gluQuadricTexture 定义是否带二次其面做纹理帖图
gluScaleImage 将图象变换为任意尺寸
gluSphere 绘制一个球体
gluTessBeginContour,gluTessEndContour 划定一个边界描述
gluTessBeginPolygon,gluTessEndPolygon 划定一个多边形描述
gluTessCallback 为镶嵌对象定义一个回调
gluTessNormal 为一个多边行形定义法向
gluTessProperty 设置镶嵌对象的属性
gluTessVertex 定义在一个多边形上的顶点
gluUnProject 将窗口坐标映射为对象坐标

2、OpenGL核心函数库
glAccum 操作累加缓冲区
glAddSwapHintRectWIN 定义一组被 SwapBuffers 拷贝的三角形
glAlphaFunc 允许设置 alpha 检测功能
glAreTexturesResident 决定特定的纹理对象是否常驻在纹理内存中
glArrayElement 定义一个被用于顶点渲染的数组成分
glBegin,glEnd 定义一个或一组原始的顶点
glBindTexture 允许建立一个绑定到目标纹理的有名称的纹理
glBitmap 绘制一个位图
glBlendFunc 特殊的像素算法
glCallList 执行一个显示列表
glCallLists 执行一列显示列表
glClear 用当前值清除缓冲区
GlClearAccum 为累加缓冲区指定用于清除的值
glClearColor 为色彩缓冲区指定用于清除的值
glClearDepth 为深度缓冲区指定用于清除的值
glClearStencil 为模板缓冲区指定用于清除的值
glClipPlane 定义被裁剪的一个平面几何体
glColor 设置当前色彩
glColorMask 允许或不允许写色彩组件帧缓冲区
glColorMaterial 使一个材质色彩指向当前的色彩
glColorPointer 定义一列色彩
glColorTableEXT 定义目的一个调色板纹理的调色板的格式和尺寸
glColorSubTableEXT 定义目的纹理的调色板的一部分被替换
glCopyPixels 拷贝帧缓冲区里的像素
glCopyTexImage1D 将像素从帧缓冲区拷贝到一个单空间纹理图象中
glCopyTexImage2D 将像素从帧缓冲区拷贝到一个双空间纹理图象中
glCopyTexSubImage1D 从帧缓冲区拷贝一个单空间纹理的子图象
glCopyTexSubImage2D 从帧缓冲区拷贝一个双空间纹理的子图象
glCullFace 定义前面或后面是否能被精选
glDeleteLists 删除相邻一组显示列表
glDeleteTextures 删除命名的纹理
glDepthFunc 定义用于深度缓冲区对照的数据
glDepthMask 允许或不允许写入深度缓冲区
glDepthRange 定义 z 值从标准的设备坐标映射到窗口坐标
glDrawArrays 定义渲染多个图元
glDrawBuffer 定义选择哪个色彩缓冲区被绘制
glDrawElements 渲染数组数据中的图元
glDrawPixels 将一组像素写入帧缓冲区
glEdgeFlag 定义一个边缘标志数组
glEdgeFlagPointer 定义一个边缘标志数组
glEnable, glDisable 打开或关闭 OpenGL 的特殊功能
glEnableClientState,glDisableClientState 分别打开或关闭数组
glEvalCoord 求解一维和二维贴图
glEvalMesh1,glEvalMesh2 求解一维和二维点或线的网格
glEvalPoint1,glEvalPoint2 生成及求解一个网格中的单点
glFeedbackBuffer 控制反馈模式
glFinish 等待直到 OpenGL 执行结束
glFlush 在有限的时间里强制 OpenGL 的执行
glFogf,glFogi,glFogfv,glFogiv 定义雾参数
glFrontFace 定义多边形的前面和背面
glFrustum 当前矩阵乘上透视矩阵
glGenLists 生成一组空的连续的显示列表
glGenTextures 生成纹理名称
glGetBooleanv,glGetDoublev,glGetFloatv,glGetIntegerv 返回值或所选参数值
glGetClipPlane 返回特定裁减面的系数
glGetColorTableEXT 从当前目标纹理调色板得到颜色表数据
glGetColorTableParameterfvEXT,glGetColorTableParameterivEXT 从颜色表中 得到调色板参数
glGetError 返回错误消息
glGetLightfv,glGetLightiv 返回光源参数值
glGetMapdv,glGetMapfv,glGetMapiv 返回求值程序参数
glGetMaterialfv,glGetMaterialiv 返回材质参数
glGetPixelMapfv,glGetpixelMapuiv,glGetpixelMapusv 返回特定的像素图
glGetPointerv 返回顶点数据数组的地址
glGetPolygonStipple 返回多边形的点图案
glGetString 返回描述当前 OpenGl 连接的字符串
glGetTexEnvfv 返回纹理环境参数
glGetTexGendv,glGetTexGenfv,glGetTexGeniv 返回纹理坐标生成参数
glGetTexImage 返回一个纹理图象
glGetTexLevelParameterfv,glGetTexLevelParameteriv 返回特定的纹理参数的 细节级别
glGetTexParameterfv,glGetTexParameteriv 返回纹理参数值
glHint 定义实现特殊的线索
glIndex 建立当前的色彩索引
glIndexMask 控制写色彩索引缓冲区里的单独位
glIndexPointer 定义一个颜色索引数组
glInitName 初始化名字堆栈
glInterleavedArrays 同时定义和允许几个在一个大的数组集合里的交替数组
glIsEnabled 定义性能是否被允许
glIsList 检测显示列表的存在
glIsTexture 确定一个名字对应一个纹理
glLightf,glLighti,glLightfv,glLightiv 设置光源参数
glLightModelf,glLightModeli,glLightModelfv,glLightModeliv 设置光线模型参数
glLineStipple 设定线点绘图案
glLineWidth 设定光栅线段的宽
glListBase 为 glcallList 设定显示列表的基础
glLoadIdentity 用恒等矩阵替换当前矩阵
glLoadMatrixd,glLoadMatrif 用一个任意矩阵替换当前矩阵
glLoadName 将一个名字调入名字堆栈
glLogicOp 为色彩索引渲染定义一个逻辑像素操作
glMap1d,glMap1f 定义一个一维求值程序
glMap2d,glMap2f 定义一个二维求值程序
glMapGrid1d,glMapGrid1f,glMapgrid2d,glMapGrid2f 定义一个一维或二维网 格
glMaterialf,glMateriali,glMateriafv,glMaterialiv 为光照模型定义材质参数
glMatrixMode 定义哪一个矩阵是当前矩阵
glMultMatrixd,glMultMatrixf 用当前矩阵与任意矩阵相乘
glNewList,glEndList 创建或替换一个显示列表
glNormal 设定当前顶点法向
glNormalPointer 设定一个法向数组
glOrtho 用垂直矩阵与当前矩阵相乘
glPassThrough 在反馈缓冲区做记号
glPixelMapfv,glPixelMapuiv,glPixelMapusv 设定像素交换图
glPixelStoref,glpixelStorei 设定像素存储模式
glPixelTransferf,glPixelTransferi 设定像素存储模式
glPixelZoom 设定像素缩放因数
glPointSize 设定光栅点的直径
glPolygonMode 选择一个多边形的光栅模式
glPolygonOffset 设定 OpenGL 用于计算深度值的比例和单元
glPolygonStipple 设定多边形填充图案
glPrioritizeTextures 设定纹理固定的优先级
glPushAttrib,glPopAttrib 属性堆栈的压入和弹出操作
glPushClientAttrib,glPopClientAttrib 在客户属性堆栈存储和恢复客户状态值
glPushmatrix,glPopMatrix 矩阵堆栈的压入和弹出操作
glPushName,glPopName 名字堆栈的压入和弹出操作
glRasterPos 定义像素操作的光栅位置
glreadBuffer 为像素选择一个源色彩缓冲区
glReadPixels 从帧缓冲区读取一组数据
glRectd,glRectf,glRecti,glRects,glRectdv,glRectfv,glRectiv,glRectsv 绘制一个三 角形
glRenderMode 定义光栅模式
glRotated,glRotatef 将旋转矩阵与当前矩阵相乘
glScaled,glScalef 将一般的比例矩阵与当前矩阵相乘
glScissor 定义裁减框
glSelectBuffer 为选择模式值建立一个缓冲区
glShadeModel 选择平直或平滑着色
glStencilFunc 为模板测试设置功能和参照值
glStencilMask 控制在模板面写单独的位
glStencilOp 设置激活模式测试
glTexCoord 设置当前纹理坐标
glTexCoordPointer 定义一个纹理坐标数组
glTexEnvf,glTexEnvi,glTexEnvfv,glTexEnviv 设定纹理坐标环境参数
glTexGend,glTexgenf,glTexGendv,glTexGenfv,glTexGeniv 控制纹理坐标的生成
glTexImage1D 定义一个一维的纹理图象
glTexImage2D 定义一个二维的纹理图
glTexParameterf,glTexParameteri,glTexParameterfv,glTexParameteriv 设置纹理参数
glTexSubImage1D 定义一个存在的一维纹理图像的一部分,但不能定义新的纹理
glTexSubImage2D 定义一个存在的二维纹理图像的一部分,但不能定义新的纹理
glTranslated,glTranslatef 将变换矩阵与当前矩阵相乘
glVertex 定义一个顶点
glVertexPointer 设定一个顶点数据数组
glViewport 设置视窗

启动模式

  • DEFAULT:默认启动模式,协程会在创建后立即开始调度,如果在调度前协程被取消,其将直接进入取消响应的状态
  • LAZY:懒启动模式,协程被调用才会启动执行,包括调用协程的start,join或者await等方法时才会被调度,如果调度前就取消那么该协程将直接进入异常结束状态
  • ATOMIC:原子启动模式,立即开始调度,在执行第一个挂起点之前协程不能被取消
  • UNDISPATCHED:不受限的启动模式,协程创建后立即在当前函数调用栈中执行,直到遇到第一个真正挂起的点才会切线程
    立即调度:表示协程的调度器会立即接收调度指令,但具体执行的时机以及在哪个线程上执行,还需要根据调度器的具体情况而定

调度器

  • Default:默认调度器,适合处理后台计算,是一个CPU密集型任务调度器
  • Main:UI调度器,平台相关,在Android上是主线程
  • Unconfined:无所谓调度器,挂起点恢复执行时会在恢复所在的线程上直接执行,如A(创建协程的线程)-B(协程挂起的线程)-B(协程恢复后执行的线程)
  • IO:IO调度器,适合执行IO相关操作,是一个IO密集型任务调度器

上下文 CoroutineContext

  • 可通过实现AbstractCoroutineContextElement抽象类自定义上下文
  • 常用的有AndroidExceptionPreHandler、CoroutineName
  • Job的作用,没Job实例的协程不能被取消

拦截器

可作为上下文的一部分

可以拦截协程异步回调时的恢复调用,即可以在挂起点恢复执行的位置添加拦截器实现AOP操作。

作用域

作用域的概念,主要用以明确协程之间的父子关系,以及对于取消或者异常处理等传播行为。
协程作用域包括以下三种:

  1. 顶级作用域:没有父协程的协程所在的作用域为顶级作用域
  2. 协同作用域:协程中启动新的协程,新协程为所在协程的子协程,这种情况下子协程所在的作用域默认为协同作用域。子协程抛出的未捕获异常都将传递给父协程处理,父协程同时也会被取消
  3. 主从作用域:与协同作用域在协程的父子关系上一致,区别在于处于该作用域下的协程出现未捕获的异常时不会将异常向上传递给父协程
    主从作用域可通过阻断子协程未捕获的异常向上传播实现,如supervisorScope()

除这三种作用域中提到的行为外,父子协程之间还存在以下规则:

  1. 父协程被取消,所有子协程均被取消
  2. 父协程需要等待子协程执行完毕之后才能最终进入完成状态
  3. 子协程会继承父协程的协程上下文中的元素,如果自身有相同的key成员,则覆盖父协程对应的key,覆盖效果仅限子协程作用域

启动协程的方式

launch

  • 返回Job对象,但无法获取协程运行结果

async

  • 返回Deffer对象,可以获取协程运行结果

Deffer中await()方法的作用:

  • 在协程已经执行完成时,立即返回协程执行的结果,如果协程异常结束,则抛出异常
  • 如果协程尚未执行完成,则挂起直到协程执行完成,与join类似

挂起与恢复

挂起的本质是程序执行流程发生异步调用时,当前调用流程的执行状态进入等待状态。
异步调用发生与否,取决于恢复调用函数与对应的挂起函数的调用是否在相同的调用栈上,切换函数调用栈的方法可以是切换到其他线程上执行,也可以是不切换线程但在当前函数返回之后的某一个时刻再执行。

  • 协程内部挂起函数的调用处被称为挂起点,挂起点出现异步调用则当前协程被挂起,直到对应的Continuation的恢复调用函数被调用才会恢复执行
  • 挂起标记 - return非阻塞式挂起当前线程
  • invokeSuspend、resume - 恢复调用

挂起的本质

常用

suspendCoroutine

  • 可处理异步回调的逻辑

suspendCancellableCoroutine

  • 可在协程被取消时收到回调 CancellableContinuation#invokeOnCancellation

Channel

Flow

  • 冷流:有观察者消费时才执行
  • 热流:马上执行

Select

参考文章:Kotlin协程

与其他图片格式对比

WebP是一种由Google开发的现代图像格式,旨在提供更高的压缩率和更好的图像质量,同时保持与现有图像格式相似的视觉质量。与传统的JPEG、PNG和GIF格式相比,WebP具有以下优缺点:

优点:

  1. 更高的压缩率:WebP通常比JPEG格式具有更高的压缩率,可以减小图像文件的大小,加快网页加载速度。
  2. 更好的图像质量:相同文件大小下,WebP格式的图像质量通常比JPEG更好,可以减少图像的失真。
  3. 支持透明度:WebP格式支持透明度,可以创建带有半透明效果的图像,而JPEG不支持透明度。
  4. 动态图像支持:WebP格式支持动态图像,可以替代GIF格式,提供更高的压缩率和更好的图像质量。

缺点:

  1. 兼容性:WebP格式相对较新,不是所有的浏览器和应用程序都支持该格式,可能会导致兼容性问题。
  2. 编解码速度:WebP格式的编解码速度可能比JPEG和PNG慢一些,特别是在处理大型图像时。
  3. 动态图像性能:虽然WebP支持动态图像,但在某些情况下可能性能不如GIF格式。

总的来说,WebP格式在压缩率和图像质量方面有优势,特别适合用于网页图像的优化。但在一些特定情况下,如兼容性和动态图像性能方面可能存在一些缺点。

压缩处理

生成WebP图像通常使用Google的官方工具cwebp命令行工具或libwebp C库。
cwebp支持许多参数来控制输出的WebP图像的质量和大小。以下是一些常用的参数:

  • -q <float>:设置输出的图像质量,范围是0(最差)到100(最好)。默认值是75。
  • -r: 帧率
  • -m <int>:设置压缩效率,范围是0(最快)到6(最慢)。更高的值会产生更小但需要更长时间来编码的文件。默认值是4。
  • -lossless:生成无损的WebP图像。
  • -alpha_q <int>:设置透明度通道的质量。更低的值会产生更小的文件但透明度质量更差。
  • -resize <width> <height>:改变图像的大小到指定的宽度和高度。
  • -o <file>:指定输出文件的名称。
  • -v:显示详细的编码信息。
  • -h:显示帮助信息。

例如,以下命令将JPEG图像转换为质量为80的WebP图像:

1
cwebp -q 80 input.jpg -o output.webp

注意,cwebp的所有参数都是可选的。如果你不提供任何参数,cwebp将使用默认的设置来生成WebP图像。

另外,ffmpeg中也集成了cwebp,可以通过以下命令将视频文件转换成WebP图像:

1
ffmpeg -i input.mp4 -vcodec libwebp -vf scale=480:640 -lossless 0 -loop 0 -r 9 -q 8 output.webp

实践应用

背景:线上能将视频文件转成webp动图的工具的转换过程对用户来说都是一个黑盒操作,我们在使用的时候无法控制转换的参数,如转换的帧率、质量等等,这导致有时得到的webp动图不合期望,故这里开发了一个将视频文件转换成webp动图的mac桌面版工具。

项目链接
https://github.com/Darrenyuen/WebPTransTool

相关技术

  • KMP:开发桌面工具
  • 集成ffmep可执行文件:其中集成的cwebp提供了转换能力

效果展示

  1. 导入需要转换的原始视频,目前仅支持mp4文件和mov文件

  2. 导入后会弹出配置选择弹窗,默认配置是帧率为9、质量为8、使用旧编解码器,转换耗时长与视频大小及配置有关
    !

  3. 转换完成后可以看到

ViewModel是Google近年强推的开发最佳实践工具包Jetpack中的重要且常被开发者使用的一员,用法比较简单,网上有许多介绍了其用法的文章,故此文不再介绍其用法。本文会从源码角度尝试解答ViewModel的几个相关问题:

  1. ViewModel实例如何创建与存储
  2. 什么时候会清空数据
  3. 为何配置发生变化的销毁不会导致数据清空
  4. ViewModel如何避免造成内存泄漏
  5. AndroidViewModel怎么做到不会发生内存泄漏的

1. ViewModel实例如何创建与存储

一切得从获取ViewModel具体实例的方法开始说起。
val viewModel = ViewModelProvider(this).get(TestViewModel::class.java) 为例说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ViewModelProvider {

private final Factory mFactory; // 用于构造ViewModel实例
private final ViewModelStore mViewModelStore; // 用于存储ViewModel实例

// 构造ViewModelProvider对象,一般Activity or Fragment都实现了ViewModelStoreOwner接口和ViewModelProvider.Factory接口
public ViewModelProvider(@NonNull ViewModelStoreOwner owner) {
this(owner.getViewModelStore(), owner instanceof HasDefaultViewModelProviderFactory
? ((HasDefaultViewModelProviderFactory) owner).getDefaultViewModelProviderFactory()
: NewInstanceFactory.getInstance());
}

public ViewModelProvider(@NonNull ViewModelStore store, @NonNull Factory factory) {
mFactory = factory;
mViewModelStore = store;
}
}

在构造ViewModelProvider对象实例时,ViewModelProvider会通过入参获取到ViewModel实例的创建者和存储者。接下来看看ViewModel实例是如何被创建及存储的。

1
2
3
4
5
6
7
8
public <T extends ViewModel> T get(@NonNull Class<T> modelClass) {
// 把传入的类名组成一个key去读取ViewModel实例
String canonicalName = modelClass.getCanonicalName();
if (canonicalName == null) {
throw new IllegalArgumentException("Local and anonymous classes can not be ViewModels");
}
return get(DEFAULT_KEY + ":" + canonicalName, modelClass);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {  
ViewModel viewModel = mViewModelStore.get(key);
// 会在ViewModelStore的HashMap<String, ViewModel>类型成员mMap中通过组合的key去读取
if (modelClass.isInstance(viewModel)) {
if (mFactory instanceof OnRequeryFactory) {
((OnRequeryFactory) mFactory).onRequery(viewModel);
}
// 实例存在且合理,则返回读取到的ViewModel实例
return (T) viewModel;
} else {
//noinspection StatementWithEmptyBody
if (viewModel != null) {
// TODO: log a warning.
}
}
if (mFactory instanceof KeyedFactory) {
viewModel = ((KeyedFactory) mFactory).create(key, modelClass);
} else {
// 通过类反射的方式去构造实例
viewModel = mFactory.create(modelClass);
}
// 存储实例
mViewModelStore.put(key, viewModel);
return (T) viewModel;
}

总结:在获取ViewModel的时候会先从ViewModelStore中的HashMap数据结构中读取实例,如果ViewModelStore中不存在该实例则通过反射方式创建实例并且存储到ViewModelStore中。

2. 什么时候会清空数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// androidx.lifecycle.ViewModel
protected void onCleared() {
}

@MainThread
final void clear() {
mCleared = true;
if (mBagOfTags != null) {
synchronized (mBagOfTags) {
for (Object value : mBagOfTags.values()) {
closeWithRuntimeException(value);
}
}
}
onCleared();
}

// androidx.lifecycle.ViewModelStore
public final void clear() {
for (ViewModel vm : mMap.values()) {
vm.clear();
}
mMap.clear();
}

ViewModel数据清空即被调用onCleared方法,而ViewModel的onCleared方法只会在ViewModelStore#clear中会被调用,阅读源码发现这意味着ViewModelStore的HashMap成员中的ViewModel,而这只会发生在宿主Activity#onDestroy或Fragment被移除时:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// androidx.activity.ComponentActivity
getLifecycle().addObserver(new LifecycleEventObserver() {
@Override
public void onStateChanged(@NonNull LifecycleOwner source,
@NonNull Lifecycle.Event event) {
if (event == Lifecycle.Event.ON_DESTROY) {
// Clear out the available context
mContextAwareHelper.clearAvailableContext();
// And clear the ViewModelStore
if (!isChangingConfigurations()) { // notice!!!
getViewModelStore().clear();
}
}
}
});

总结:会在宿主Activity或Fragment被销毁时清空数据
Google官方对于ViewModel生命周期与Activity生命周期的关系说明:

3. 为何配置发生变化的销毁不会导致数据清空

从上面notice!!!标注的代码行中可以发现,只有是非配置变化导致的Activity#onDestroy才会触发ViewModelStore的clear方法!

4. ViewModel如何避免造成内存泄漏

经分析,我们已经知道在ViewModel的宿主Activity或Fragment在被销毁时会调用ViewModel#clear,但假设我们在ViewModel中持有的宿主的引用,而又不在clear时释放引用,那此时便会造成内存泄漏。

一些避免ViewModel造成内存泄漏的建议和做法:

  1. 避免持有宿主的引用
  2. 在ViewModel#clear时释放宿主的引用、取消网络请求、关闭数据库连接,如:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class MyViewModel : ViewModel() {
    private var activityRef: WeakReference<Activity>? = null

    fun setActivity(activity: Activity) {
    activityRef = WeakReference(activity)
    }

    fun clear() {
    activityRef?.clear()
    activityRef = null
    }
    }

5. AndroidViewModel怎么做到不会发生内存泄漏的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class AndroidViewModel extends ViewModel {  
@SuppressLint("StaticFieldLeak")
private Application mApplication;

public AndroidViewModel(@NonNull Application application) {
mApplication = application;
}

/**
* Return the application.
*/
@SuppressWarnings("TypeParameterUnusedInFormals")
@NonNull
public <T extends Application> T getApplication() {
return (T) mApplication;
}
}

来看下AndroidViewModel的源码,AndroidViewModel是ViewModel的一个子类,它提供了一个Application对象的引用,可以在AndroidViewModel中使用Application Context而不是Activity的Context来避免内存泄漏。这是因为Application Context的生命周期与应用程序的生命周期相同,而Activity的生命周期是短暂的,当Activity被销毁时,它的Context也会被销毁,如果在ViewModel中使用了Activity的Context,就可能会导致内存泄漏。

本人所作的内部技术分享,脱敏同步

项目介绍

项目背景

为了解决视频转WebP动图的痛点问题

  1. 用的是外部工具,黑盒过程不可控
  2. webp体积过大,存在OOM风险
  3. 帧率、质量没有统一标准

期望提供一个视频转webp工具约束生产流程

方案选型

业务后台(链路长) vs 脚本工具(门槛高) vs KMP Compose(技术栈匹配能内部闭环)

建设过程

新建项目方式:

  1. 官方模板代码:https://github.com/JetBrains/compose-multiplatform-template
  2. Kotlin Multiplatform Mobile plugin

实践项目结构介绍

Compose编程关键点

•composable 可组合的UI

•mutableStateOf 更新发起点

•remember 刷新前后状态继承

1
2
3
4
5
6
7
8
9
val taskList = remember { mutableStateOf(LinkedList<TaskData>()) } // list是更新点且可继承状态

Box(Modifier.wrapContentHeight(), contentAlignment = Alignment.TopCenter) { ListTaskView(taskList.value)
} // view data bind

taskData.value = TaskData(inputPath = inputPath.value, outputPath = outputPath.value)taskList.value.addFirst(taskData.value) // list新增一个条目 

// 条目状态变更
taskData.value.inputPath = filePathtaskData.value.outputPath = outputPathtaskData.status.value = ConvertSuccess

KMP介绍

KMM, KMP, KMP Compose到底都是个啥?

KMM(Kotlin Multiplatform Mobile)

KMP(Kotlin Multiplatform)

KMP Compose(Kotlin Multiplatform Compose)

跨平台框架对比

Flutter React Native Kotlin Multiplatform
UI支持 较强 较强
开发语言 Dart Javascript Kotlin
迁移成本
运行环境 Flutter引擎 JSCore 纯原生
性能 较高 一般

KMP、Flutter和React Native都是用于开发跨平台移动应用的框架,各有其优缺点。

KMP发展趋势

应用阵营:百度、快影、McDonald’s、Netflix… https://www.jetbrains.com/help/kotlin-multiplatform-dev/case-studies.html

KMP跨平台原理

KMP 的核心原理是通过 Kotlin K2编译器将共享的 Kotlin 代码编译成适用于目标平台的原生代码,从而实现在不同平台上共享代码的目的。

项目结构与运行

项目结构


共享模块中的平台命名文件和平台特定模块中的文件有何区别?平台特定模块更多是作为一个应用入口。

依赖管理

公共依赖(e.g. Koin, Apollo, Okio

特定平台依赖 (e.g. Glide, Fresco, AFNetworking, YYModel)

构建运行

项目形态

  • 共享一段逻辑
    共享应用的独立共同逻辑。
  • 共享逻辑但保持原生UI
    开始新项目时使用Kotlin Multiplatform,只需实现一次数据处理和业务逻辑,并且保持 UI 原生。
  • 共享逻辑与UI
    使用Compose Multiplatform提高开发效率,共享逻辑与UI的代码。

What’s more

官方示例:https://github.com/android/kotlin-multiplatform-samples

Reference

KMP Development
跨平台开发的后起之秀
Kotlin Multiplatform Mobile 跨平台原理

由于一进入工作状态进长期久坐盯着电脑屏幕,长期以往始觉肩酸眼涩。所以想开发一个运行于IDE中的定时提醒插件,定时提醒自己离开座位远眺放松。

新建项目

Android Studio是基于IntelliJ IDEA构建的,因此要开发的也是基于IDEA平台的插件。插件的模板代码可以在IDEA上新建项目时勾选,也可以在官网上克隆使用:

  1. 在IDEA中创建一个新的插件项目:New Project -> Generators IDE Plugin -> Project Name -> Type choose Plugin not Theme
  2. 在官方仓库上克隆使用:IDEA platform plugin template
    官方文档中提到仓库中的模板代码预设了gradle配置支持,使用IDE因此本次实践是使用官方仓库上的模板代码。

开发插件

  1. 运行模板代码
    把官方模板仓库中的代码克隆下来,使用IDEA打开,无需额外设置其他配置,只需要等sync完成后选择run plugin的gradle选项即可运行。模板代码中预设的插件名是MyToolWindow,运行效果如图:

  2. 绘制控件
    本意是做一个定时提醒插件,分析需求后,只需要以下几个交互控件:

  • 设置提醒时间间隔
  • 确认提醒按钮
  • 取消提醒按钮
  • 提醒控件
    使用者可以设置提醒时间间隔,确认提交后定时提醒任务便开始运行,每隔指定时间会在IDE右下角进行弹窗提醒;用户可以通过取消提醒来取消提醒任务。
    IDEA插件是基于Swing使用Java/Kotlin进行开发的,所以可以使用Swing中的控件库来绘制UI。基于需求绘制及运行的结果如图所示:
  1. 持久化数据
    考虑到IDE可能会被关闭重启,不想每次都要手动输入一遍提醒数据,所以需要通过持久化把设置的阈值记录下来以便下次启动的时候还可以继续使用。IDEA官方提供了持久化的接口,开发者只需要实现并声明需要持久化字段的读与写就好。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    @State(name = "state", storages = [Storage(value = "state.xml")])  
    class PersistentState private constructor(): PersistentStateComponent<PersistentState> {

    companion object {
    fun getInstance(): PersistentState {
    return ApplicationManager.getApplication().getService(PersistentState::class.java)
    }
    }

    var remindIntervalTime: Int? = null


    override fun getState(): PersistentState? {
    return this
    }

    override fun loadState(p0: PersistentState) {
    XmlSerializerUtil.copyBean(p0, this)
    }
    }

    示例中实现了官方提供的PersistentStateComponent接口,里面指明了需要持久化数据的名词及类型,并且以单例的形式提供给调用者使用。

    1
    2
    写:persistentService.remindIntervalTime = value
    读:val value = PersistentState.getInstance().remindIntervalTime
  2. 发送通知
    IDEA中的弹窗种类有很多,提醒插件的需求是在IDE右下角出现提示弹窗,所以选择使用Notification。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    object RemindNotify {  
    fun showRemindNotify() {
    Notifications.Bus.notify(
    Notification(
    "Print",
    "Remind",
    "It's time to have a rest",
    NotificationType.WARNING
    )
    )
    }
    }
  3. 更新Icon
    最后,设置插件的Icon让插件更有辨识度。在root_project/src/main/resource/META-INF中新增pluginIcon.svg文件,运行打包时,会自动应用其为插件ICON。

打包插件

将插件打包成jar或zip文件,并发布到插件市场或其他适当的位置。打包后的文件存在于root_project/build/distributions文件夹下。本地使用方式是在IDE中进入插件配置页应用硬盘中的插件:Settings->Plugins->Manager->Install Plugin from disk->选择zip包->apply->重启IDE便能生效。

项目地址

https://github.com/Darrenyuen/Reminder-Plugin

参考

How to build a plugin for IDEA-based platform
https://zhuanlan.zhihu.com/p/608825921

在一次需求中需要实现播放卡片无限滚动的效果,现有代码使用的是Viewpager2+Fragment的框架,但官方的Viewpager2并没有提供无限滑动的API,所以这里摸索了一套方案记录如下。

在动手coding之前在网上调研了相关效果的实现方案,大致有两种思路:一种是在FragmentStateAdapter#getItemCount()中返回Integer.MAX_VALUE通过不断填充内容使得滑不动尽头达到无限滑动的效果;另一种则是在列表前后各加一个元素,再配合自动切换实现无限滑动。需求实现中采用的是第二种思路。

改造数据源

前后各加一个元素的具体实现,是在构造Adapter数据的时候分别将数据列表的最后一个元素和第一个元素拷贝添加至数据列表的头部和尾部。即数据列表的内容由[A, B, C] 变为 [C, A, B, C, A]。

Viewpager2自动切换

注册Viewpager2页面状态改变的监听器,在回调中判断如果当前处于第一张卡片(即C`处)则自动切换到倒数第二张卡片(即C处),如果当前当前处于最后一张卡片则自动切换到第二张,通过这样一种方式可以让用户无限滑动卡片。

相关核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 改造数据源,原数据为[RED, YELLOW, BLUE]
dataList = arrayListOf<Int>(Color.BLUE, Color.RED, Color.YELLOW, Color.BLUE, Color.RED)

// 在viewpager2的页面状态监听器中实现自动切换
viewPager2?.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
override fun onPageScrolled(
position: Int,
positionOffset: Float,
positionOffsetPixels: Int
) {
super.onPageScrolled(position, positionOffset, positionOffsetPixels)

if (positionOffset != 0f) return

// itemCount > 1要支持循环滑动
if (itemCount > 1) {
if (position == 0) {
viewPager?.setCurrentItem(itemCount - 2, false)
} else if (position == itemCount - 1) {
viewPager?.setCurrentItem(1, false)
}
}
}
})

该示例代码在Demo中运行是能达到预期效果的,但是当在主工程上实现时发现[A, B, C] to [C, A, B, C, A]这个对数据源的改造出现了水土不服,原因是Adapter中有一个通过songID获取当前索引的方法:

1
2
3
fun findPosition(songId: String): Int {
return dataList.indexOfFirst { it.songId == songId }
}

所以存在一个问题,即在 [C, A, B, C, A]中,当想获取C的位置时返回的却是C`的索引。这个方法在原有代码中被多处调用,改造这个方法也存在一定风险,本着不动现有逻辑的原则,唯有探索其他方法了。

之所以这一种方案会出现水土不服,是因为改造数据源时对数据元素进行了深拷贝,导致执行findPostion可能出错。能不能避开通过深拷贝的方式来适配当前的findPosition逻辑呢?答案是肯定的。

优化后的数据源

既然通过深拷贝元素对象不可行,那干脆不进行对象的拷贝了。在数据列表前后各加一个元素这个方案优化后的数据源实现方式是在数据列表前后各加一个元素类型的空对象,即只创建元素对象,不对其进行使用。

1
2
3
4
5
6
7
8
if (list.size > 1) {
// 构造循环列表 [A, B, C] to [obj, A, B, C, obj]
dataList.add(PlaySongInfo())
dataList.addAll(list)
dataList.add(PlaySongInfo())
} else {
dataList.addAll(list)
}

由于改造后的数据源有前后各有一个仅作填充使用的对象,所以在使用列表数据的时候需要做一下index的转换,以下是使用数据源创建fragment的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
override fun createFragment(position: Int): Fragment {
val dataSize = dataList.size
// 卡片大于1时支持循环滑动
val realPosition = if (dataSize > 1) {
if (position == 0) {
dataSize - 2
} else if (position == dataSize - 1) {
1
} else {
position
}
} else {
position
}
val songInfo = dataList[realPosition]
return TargetFragment.newInstance(songInfo)
}

至此,优化后的数据源再配合上Viewpager2状态监听器中的自动切换逻辑,播放卡片无限滑动的效果就实现了。

Tool

Skill

  • 实时导出日志:adb logcat > logcat.log
  • adb shell am start -D -n com.darrenyuen.demo/.DemoActivity 该命令会等debug attach后才启动指定acitivity
  • git bisect good/bad 二分定位出问题的commit
  • adb logcat -b crash 打印最近一次的crash堆栈

模块化与组件化

提及模块化,常常会想到组件化,这两者都是软件开发中的设计理念,相似但不尽相同。

  • 组件化:组件相当于库,组件化是指把一些能在项目里或者不同类型项目中可复用的代码进行工具性的封装,抽离为单个组件,按照功能逻辑进行划分并控制粒度便于项目的维护和开发。多个组件也可以组合成组件库,方便调用和复用,组件间也可以嵌套,小组件组合成大组件。
  • 模块化:更多从业务逻辑出发,分属同一功能/业务的代码进行隔离(分装)成独立的模块,,让业务逻辑在模块内内聚,可以独立运行,以页面、功能或其他不同粒度划分程度不同的模块,位于业务框架层,模块间通过接口调用,目的是降低模块间的耦合,由之前的主应用与模块耦合,变为主应用与接口耦合,接口与模块耦合。

为什么要模块化

  • 提升编译速度:模块化可以将一个大的项目分解为多个小的模块,每个模块都可以独立编译。这样,当你修改了某个模块的代码后,只需要重新编译这个模块,而不需要重新编译整个项目。这大大减少了编译的时间,从而提高了编译速度。
  • 提升开发效率:模块化可以使每个模块都可以独立进行开发测试,开发者可以专注于模块内的开发;同时,模块化通过分工合作、代码重用、模块独立和易于扩展来提升开发效率。
  • 可维护性强:同一业务的代码在模块内聚合,每个模块都有自己的职责和功能。这样,当一个模块出现问题时,只需要修复该模块,而不需要影响整个应用程序。

  • 提高可扩展性:模块化可以将应用程序拆分成多个独立的模块,每个模块都可以单独扩展或与其他模块组合扩展。这样可以提高应用程序的可扩展性,使其更容易适应不同的需求和场景。

模块化实践方案

模块化中的重要课题是:

  1. 主模块与子模块解耦合,在安卓工程上的表现是主模块要依赖子模块来使用子模块中的能力,因此主模块可以依赖子模块,但子模块不能依赖主模块,否则就是非法环形依赖了。
  2. 子模块与主模块通信,解耦合带来的问题是在子模块中不能依赖主模块,也就不能直接使用主模块中的能力了,因此需要解决特定情况下子模块使用主模块能力的问题。

本文介绍两种常见常用的模块化方案。

以下示例都假设有主模块(app)、接口模块(api)和子模块(impl)三种模块角色

隐式依赖(SPI)

SPI(service provider interface),通过使用Java提供的ServiceLoader接口,在工程结构的module/resources/META-INF/services目录里创建服务接口同名文件,文件内是实现类的包路径,从而在运行时动态通过该服务接口名字查找到实现类的名字,并装载实例化,完成模块的注入。

逻辑关系如图所示:

具体实现:

  1. 在接口模块中声明接口
    1
    2
    3
    4
    5
    package com.example.spi.api  

    interface ILogInterface {
    fun logD(msg: String)
    }
  2. 在实际业务模块中实现接口
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    package com.example.spi.impl  

    import android.util.Log
    import com.example.spi.api.ILogInterface

    class Logger: ILogInterface {
    companion object {
    private const val TAG = "Logger"
    }
    override fun logD(msg: String) {
    Log.d(TAG, msg)
    }
    }
  3. 在接口模块中的resources/META-INF/services目录里创建服务接口同名文件,接口同名文件内部是实现类的包路径
  4. 在主模块中通过ServiceLoader与接口模块中的接口使用实际业务模块中的能力
    1
    ServiceLoader.load(ILogInterface::class.java, classLoader).iterator().next()?.logD("test")

显式依赖

显示依赖也是借助运行时反射来达到依赖的,但这交由开发者来手动实现,并非同spi方式只需在接口模块编写配置文件即可。

逻辑关系如图所示:

具体实现:

  1. 同样地在接口模块中声明接口,主模块与子模块不直接通信,而是通过接口模块这个桥梁实现通信

    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 子模块通过接口模块提供给主模块的能力,由子模块实现
    interface IService {
    fun getSubActivity(): Activity
    }

    // 主模块通过接口模块提供给子模块的能力,由主模块实现
    interface IContext {
    val util: IUtil
    }
  2. 在子模块中实现接口,并提供接口接受主模块注入的能力

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 子模块实现接口  
    @Keep
    class ServiceImpl(context: IContext): IService {

    init {
    HostContextForService.init(context) // 接受主模块注入的能力,后续通过访问HostContextForService即可使用主模块的能力
    }

    // 向外暴露子模块的能力
    override
    fun getSubActivity(): Activity {
    return SubActivity()
    }
    }
  3. 主模块通过接口模块使用子模块的能力,并注入主模块的能力

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // 实现接口,向子模块提供能力
    class ContextImpl: IContext {
    override val util: IUtil
    get() = Util()
    }

    // 使用子模块的能力
    fun initService() {
    runCatching {
    if (ServiceManager.getService(IService::class.java) == null) {
    val subModuleHost = ContextImpl() // 向子模块提供能力
    // 运行时构造子模块中的实例,并注入主模块的能力
    val serviceImplClass = Class.forName("com.example.impl.service.ServiceImpl")
    val constructor = serviceImplClass.getConstructor(IContext::class.java)
    val serviceImpl = constructor.newInstance(subModuleHost) as IService
    ServiceManager.registerIfNull(IService::class.java, serviceImpl)
    // 后续通过 ServiceManager获取子模块的实例并使用子模块
    }
    }.onFailure {
    Log.e("ServiceRegistry", "init template show service error.", it)
    }
    }

至此,便实现了主模块与子模块间的通信,并且可以在子模块中不用依赖主模块也能使用主模块中的能力了。

可能有人会觉得需要手动显示依赖的方式会比较麻烦,但正因为显式反射依赖,开发者可以在反射构造子模块中实例时注入主模块中的一些能力,这样能更方便地实现从主模块拆分子模块的模块化工作。

总结

本文介绍了模块化的两种方案,两种方案都是除了主模块与子模块外还有一个接口层模块,接口层模块的存在可以更好的隔绝主模块与子模块,以实现更好的解耦合。
其中,第一种隐式依赖,主模块能通过语言级的spi能力快速与子模块通信,但无法向子模块注入主模块中的能力;第二种显式依赖,需要开发者手动编写运行时反射代码以实现主模块对子模块的依赖,但可以在反射时注入主模块的一些能力,来让子模块也能调用主模块的一些能力。