0%

Kotlin 的泛型和 Any 都是 Kotlin 语言中的重要概念,它们在处理类型和数据时有不同的用法和作用。下面详细说明它们的联系和区别:

联系

  1. 类型参数的上界: 每个 Kotlin 的泛型类型参数都有一个默认的上界,即 Any?。这意味着如果你没有指定泛型类型参数的上界,默认情况下它可以是任何类型,包括 null

    1
    class Box<T>(val value: T)  // T 的默认上界是 Any?
  2. 兼容性: 泛型类型参数可以指定上界为 Any,这意味着它只能接受非空类型。

    1
    class Box<T : Any>(val value: T)  // T 的上界是 Any,所以 T 不能是 null

区别

  1. 基本概念:
  • 泛型(Generics):提供了类型参数化能力,使得类和函数可以处理多个不同的类型,而不需要在定义时指定具体的类型。

    1
    class Box<T>(val value: T)  // T 是一个泛型类型参数
  • AnyAny 是所有非空类型的根类型,相当于 Java 的 ObjectAny? 是所有类型(包括 null)的根类型。

  1. 类型安全和约束:
  • 泛型类型可以指定上界来约束它可以接受的类型。没有指定上界的泛型类型参数的上界默认是 Any?,即允许任何类型,包括 null
1
class Box<T : Number>(val value: T)  // T 的上界是 Number,所以只能接受数字类型
  • Any 只能接受非空类型,如果需要允许空值,应该使用 Any?
1
2
3
4
5
6
7
fun printValue(value: Any) {  // 只接受非空的任何值
println(value)
}

fun printValue(value: Any?) { // 可以接受任何值,包括 null
println(value)
}
  1. 类型擦除: Kotlin 的泛型在运行时类型信息会被擦除,类型参数信息只在编译期间有效。而 Any 是具体的类型,不会被类型擦除。
1
2
3
4
5
6
7
fun <T> isString(value: T): Boolean {
return value is String // 会出现类型擦除的问题,此处 is String 仍然会编译通过但有警告
}

fun isString(value: Any): Boolean {
return value is String // 不会被类型擦除,运行时仍然有效
}
  1. 类型参数的使用: 泛型类型参数可以在类、接口和函数中使用,使得代码更加通用和灵活。
1
2
3
4
5
6
7
class Container<T>(val content: T) {
fun getContent(): T = content
}

fun <T> createContainer(item: T): Container<T> {
return Container(item)
}

总结来说,Kotlin 的泛型提供了类型参数化的能力,使代码更灵活和可重用,而 Any 是所有非空类型的根类型,提供了对象类型的基本功能。泛型在编译期间处理,而 Any 在运行时也有效,两者在处理类型时有不同的侧重点和用法。

Kotlin 的泛型系统支持定义上界和下界约束,包括协变和逆变。这些特性使得 Kotlin 的泛型类型系统更加灵活和强大。下面是对 Kotlin 泛型上界和下界的详细说明:

泛型的上界 (Upper Bounds)

上界用于限制泛型类型参数的类型范围。默认情况下,泛型类型参数具有一个隐式的上界 Any?。你可以通过显式声明上界来进一步限制类型参数的类型范围。

示例

  1. 默认隐式上界

    1
    class Box<T>(val value: T)  // T 的隐式上界是 Any?
  2. 显式上界

    1
    class Box<T : Number>(val value: T)  // T 的上界是 Number,因此只能接受 Number 的子类型
  3. 多重上界: 如果需要使用多个上界,可以使用 where 子句来进行声明。

    1
    2
    3
    4
    5
    6
    fun <T> ensureAllNonNull(vararg args: T) where T : Any, T : Comparable<T> {
    for (arg in args) {
    checkNotNull(arg)
    println(arg)
    }
    }

泛型的下界 (Lower Bounds)

Kotlin 不直接提供显式声明下界的语法,但下界的概念可以通过协变和逆变来实现。协变和逆变是用于描述类型参数之间的子类型关系的概念,它们定义了类型参数在子类型化关系中的行为。

协变 (Covariance)

协变允许使用子类型来替代泛型类或接口的类型参数。在 Kotlin 中,协变使用关键字 out 来定义,表示泛型类型参数只能出现在输出位置(返回类型),不能出现在输入位置(函数参数)。

1
2
3
4
5
6
7
interface Source<out T> {
fun nextT(): T
}

fun demo(source: Source<String>) {
val anySource: Source<Any> = source // 协变,String 是 Any 的子类型,所以 Source<String> 可以赋值给 Source<Any>
}

逆变 (Contravariance)

逆变允许使用超类型来替代泛型类或接口的类型参数。在 Kotlin 中,逆变使用关键字 in 来定义,表示泛型类型参数只能出现在输入位置(函数参数),不能出现在输出位置(返回类型)。

1
2
3
4
5
6
7
interface Consumer<in T> {
fun consume(item: T)
}

fun demo(consumer: Consumer<Number>) {
val intConsumer: Consumer<Int> = consumer // 逆变,Int 是 Number 的子类型,所以 Consumer<Number> 可以赋值给 Consumer<Int>
}

使用示例

以下是一个综合示例如 Box 类和函数如何利用泛型的上界和协变逆变:

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
31
32
33
34
35
36
37
38
39
40
41
42
43
// 定义一个具有上界的泛型类
class Box<T : Number>(val value: T)

fun <T : Number> sum(value1: T, value2: T): Double {
return value1.toDouble() + value2.toDouble()
}

// 定义一个协变接口
interface Source<out T> {
fun nextT(): T
}

// 定义一个逆变接口
interface Consumer<in T> {
fun consume(item: T)
}

fun main() {
// 使用具有上界的泛型类
val intBox = Box(1)
val doubleBox = Box(1.0)

println(sum(1, 2)) // 输出: 3.0
println(sum(1.0, 2.0)) // 输出: 3.0

// 示例协变
val stringSource: Source<String> = object : Source<String> {
override fun nextT(): String {
return "Hello"
}
}
val anySource: Source<Any> = stringSource
println(anySource.nextT()) // 输出: Hello

// 示例逆变
val numberConsumer: Consumer<Number> = object : Consumer<Number> {
override fun consume(item: Number) {
println("Consumed $item")
}
}
val intConsumer: Consumer<Int> = numberConsumer
intConsumer.consume(10) // 输出: Consumed 10
}

总结

  • 上界:限制泛型类型参数的类型范围,默认是 Any?,可以显式地使用 :<类型> 来指定。
  • 下界:Kotlin 没有直接的下界语法,但可以通过协变(out)和逆变(in)来实现特定的行为。
  • 协变:使用 out 关键字表示,只能用于返回类型,允许将子类型泛型对象赋值给超类型的变量。
  • 逆变:使用 in 关键字表示,只能用于输入类型,允许将超类型泛型对象赋值给子类型的变量。

开闭原则 OCP(Open-Closed Principle)

对扩展开放,对修改关闭

根据开闭原则,在设计一个软件系统模块(类,方法)的时候,应该可以在不修改原有的模块(修改关闭)的基础上,能扩展其功能(扩展开放)。

扩展开放:某模块的功能是可扩展的,则该模块是扩展开放的。软件系统的功能上的可扩展性要求模块是扩展开放的。
修改关闭:某模块被其他模块调用,如果该模块的源代码不允许修改,则该模块修改关闭的。软件系统的功能上的稳定性,持续性要求模块是修改关闭的

开闭原则的实现方法
为了满足开闭原则的对修改关闭原则以及扩展开放原则,应该对软件系统中的不变的部分加以抽象,在面向对象的设计中

  • 可以把这些不变的部分加以抽象成不变的接口,这些不变的接口可以应对未来的扩展;
  • 接口的最小功能设计原则。根据这个原则,原有的接口要么可以应对未来的扩展;不足的部分可以通过定义新的接口来实现;
  • 模块之间的调用通过抽象接口进行,这样即使实现层发生变化,也无需修改调用方的代码。

接口可以被复用,但接口的实现却不一定能被复用。
接口是稳定的,关闭的,但接口的实现是可变的,开放的。
可以通过对接口的不同实现以及类的继承行为等为系统增加新的或改变系统原来的功能,实现软件系统的柔性扩展。

迪米特法则(最少握手) LoD(Law Of Demeter or Principle of Least Knowledge)

一个类对于其他类知道的越少越好,就是说一个对象应当对其他对象有尽可能少的了解,只和朋友通信,不和陌生人说话。

迪米特原则的初衷在于降低类之间的耦合。由于每个类尽量减少对其他类的依赖,因此,很容易使得系统的功能模块功能独立,相互之间不存在(或很少有)依赖关系。

里氏替换原则 LSP(Liskov Substitution Principle)

任何超类出现的地方可以用任一派生类进行替换,实践中可用以校验继承是否有必要以及是否合法

只有满足以下2个条件的OO设计才可被认为是满足了LSP原则:

  • 不应该在代码中出现if/else之类对派生类类型进行判断的条件。
  • 派生类应当可以替换基类并出现在基类能够出现的任何地方,或者说如果我们把代码中使用基类的地方用它的派生类所代替,代码还能正常工作。

同时LSP体现了:

  • 类的继承原则:如果一个派生类的对象可能会在基类出现的地方出现运行错误,则该派生类不应该从该基类继承,或者说,应该重新设计它们之间的关系。
  • 动作正确性保证:从另一个侧面上保证了符合LSP设计原则的类的扩展不会给已有的系统引入新的错误。

重构违反LSP的设计

如果两个具体的类A,B之间的关系违反了LSP 的设计,(假设是从B到A的继承关系),那么根据具体的情况可以在下面的两种重构方案中选择一种:

  • 创建一个新的抽象类C,作为两个具体类的基类,将A,B的共同行为移动到C中来解决问题。
  • 从B到A的继承关系改为关联关系。

单一职责原则  SRP(Single Resposibility Principle)

永远不要让一个类存在多个改变的理由。

要控制类的粒度大小,将对象解耦,提高其内聚性,即一个对象不要承担太多职责,一个方法尽量做好一件事,提高其原子性。

接口隔离原则 ISP(Interface Insolation Principle)

不能强迫用户去依赖那些他们不使用的接口。

  • 按分类设计接口,避免耦合

依赖倒置原则  DIP(Dependency Inversion Principle)

A. 高层模块不应该依赖于低层模块,二者都应该依赖于抽象
B. 抽象不应该依赖于细节,细节应该依赖于抽象
C.针对接口编程,不要针对实现编程

符合DIP原则的操作:

  • 在高层模块与低层模块之间,引入一个抽象接口层
  • 避免依赖传递

组合/聚合复用原则

尽量使用组合/聚合,不要使用类继承

  • 厘清 is-a 与 has-a,区别继承与组合
  • 复用时,要尽量先使用组合或者聚合等关联关系来实现,其次才考虑使用继承关系来实现。

在面向对象设计中,有两种基本的办法可以实现复用:第一种是通过组合/聚合,第二种就是通过继承。

什么时候才应该使用继承
只有当以下的条件全部被满足时,才应当使用继承关系:

1)派生类是基类的一个特殊种类,而不是基类的一个角色,也就是区分”Has-A”和”Is-A”。只有”Is-A”关系才符合继承关系,”Has-A”关系应当用聚合来描述。
2)永远不会出现需要将派生类换成另外一个类的派生类的情况。如果不能肯定将来是否会变成另外一个派生类的话,就不要使用继承。
3)派生类具有扩展基类的责任,而不是具有置换掉(override)或注销掉(Nullify)基类的责任。如果一个派生类需要大量的置换掉基类的行为,那么这个类就不应该是这个基类的派生类。
4)只有在分类学角度上有意义时,才可以使用继承。

思考:如何培养code sense

  • 多写多想,从实践中摸索总结
  • 注重代码设计,做好设计再coding
  • 提升bad code识别能力,如重复代码的出现、hard code
  • 基于OOP原则,内化设计模式且与其常用场景结合消化,训练强化的结果

参考自:面向对象设计的七大设计原则详解

近期实现的一个功能中,由于后台需要的请求入参数据量过大,而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
2
3
4
protoc --java_out=输出目录 编译文件 -I=编译文件所在的文件夹

# 编译.proto文件为Kotlin文件
# protoc --proto_path=src/test/proto --kotlin_out=src/test/kotlin src/test/proto/addressbook.proto
  • 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
    24
    25
    26
    27
    // 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"
    }
    // Generates Python code
    // python { }
    // Generates Kotlin code
    // kotlin { }
    }
    }
    }
    }
  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()
}

参考文章:
如何在Android上从零使用Protobuf
适用于 Gradle 的 Protobuf 插件

(一)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强推的Android开发最佳实践工具包Jetpack中的重要一员,用法比较简单,网上有许多介绍了其用法的文章,故此文不再介绍其用法。本文会从源码角度尝试解答ViewModel的几个相关问题:

  1. ViewModel实例如何创建与存储
  2. 什么时候会清空数据,为何配置发生变化的销毁不会导致数据清空
  3. ViewModel如何避免造成内存泄漏
  4. 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;
}
}

Activity和Fragment都实现了ViewModelStoreOwner和HasDefaultViewModelProviderFactory接口,分别用于创建ViewModelStore实例和ViewModel实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Activity
public class ComponentActivity extends androidx.core.app.ComponentActivity implements
ContextAware,
LifecycleOwner,
ViewModelStoreOwner,
HasDefaultViewModelProviderFactory,
SavedStateRegistryOwner,
OnBackPressedDispatcherOwner,
ActivityResultRegistryOwner,
ActivityResultCaller

// Fragment
public class Fragment implements ComponentCallbacks, OnCreateContextMenuListener, LifecycleOwner,
ViewModelStoreOwner, HasDefaultViewModelProviderFactory, SavedStateRegistryOwner,
ActivityResultCaller

在构造ViewModelProvider对象实例时,ViewModelProvider会通过ViewModelStoreOwner类型入参获取到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.kt
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.kt
public final void clear() {
for (ViewModel vm : mMap.values()) {
vm.clear();
}
mMap.clear();
}

ViewModel数据清空即被调用onCleared方法,而ViewModel的onCleared方法只会在ViewModelStore#clear中会被调用,阅读源码发现ViewModelStore的clear只会发生在宿主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()) { // 非配置变化引起的destroy
getViewModelStore().clear();
}
}
}
});

总结:ViewModelStore中的map会在宿主Activity或Fragment销毁时被清理。

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

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

但有个问题,我们知道尽管是配置变化引起的Activity销毁,但新建的Activity是一个全新的对象,那这个新Activity对象是如何继承前一个Activity中的ViewModel实例的呢?换句话说,或者是如何继承前一个Activity中的ViewModelStore实例的呢?

答案就在Activity的NonConfigurationInstances类型的mLastNonConfigurationInstances成员中。
因配置变化引起的Activity销毁重建属于异常销毁重建,NonConfigurationInstances类型成员的activity会持有之前被异常销毁的activity对象,也就持有了ViewModelStore实例,在每次Activity创建包括重建时都会先从NonConfigurationInstances对象中读取,NonConfigurationInstances对象没有才会创建新的ViewModelStore实例。

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

Google官方对于ViewModel生命周期与Activity生命周期的关系说明:

经分析,我们已经知道在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
    }
    }

4. 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或Fragment的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