add TinyPngPlugin

master
Omooo 6 years ago
parent 46422ff4d7
commit f53deb0608
  1. 10
      README.md
  2. 267
      blogs/Android/Gradle/Gralde Plugin 实践之 TinyPng Plugin.md
  3. 3
      blogs/JVM/深入理解 Class 文件格式/Class 文件格式总览.md
  4. 137
      blogs/JVM/深入理解 Class 文件格式/常量池及相关内容.md

@ -46,6 +46,16 @@ Android Notes
16. [IntentService 源码分析](https://github.com/Omooo/Android-Notes/blob/master/blogs/Android/IntentService.md)
17. [View 工作原理](https://github.com/Omooo/Android-Notes/blob/master/blogs/Android/View%20%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86.md)
##### Gralde Plugin、Groovy
[Gralde Plugin 入门指南](https://github.com/Omooo/Android-Notes/blob/master/blogs/Android/Gradle/Gradle%20Plugin.md)
[Groovy 常用操作](https://github.com/Omooo/Android-Notes/blob/master/blogs/Android/Gradle/Gralde%20Plugin%20%E5%AE%9E%E8%B7%B5%E4%B9%8B%20TinyPng%20Plugin.md)
##### JVM、ART 相关
[Class 文件格式总览](https://github.com/Omooo/Android-Notes/blob/master/blogs/JVM/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3%20Class%20%E6%96%87%E4%BB%B6%E6%A0%BC%E5%BC%8F/Class%20%E6%96%87%E4%BB%B6%E6%A0%BC%E5%BC%8F%E6%80%BB%E8%A7%88.md)
##### 性能优化
[I/O 优化](https://github.com/Omooo/Android-Notes/blob/master/blogs/Android/%E6%80%A7%E8%83%BD%E4%BC%98%E5%8C%96/I%5CO%20%E4%BC%98%E5%8C%96.md)

@ -0,0 +1,267 @@
---
Gralde Plugin 实践之 TinyPng Plugin
---
#### 前言
在上一篇文章中,我们熟悉了如何去实现一个[自定义的 Gradle Plugin](https://github.com/Omooo/Android-Notes/blob/master/blogs/Android/Gradle/Gradle%20Plugin.md),本来按照计划这篇文章是讲 Transform API,但是考虑到学完新知识最好能实践一下,之前也讲到可以利用 TinyPng 在构建项目的时候批量压缩 res 下的所有 png 图片,今天我们就来实践一下,这个并不涉及到 Transform API 的使用,但是需要熟悉 Groovy 一些常见的操作,比如 Extensions、 Json 的解析和生成等,整个项目很简单,代码并不多,大胆 fork [TinyPngPlugin](https://github.com/surpriseprojects/TinyPngPlugin) 吧。
#### 实现方式
我们可以直接利用 TinyPng 给的 API 即可,文档地址为:
[https://tinypng.com/developers/reference/java](https://tinypng.com/developers/reference/java)
核心源码如下:
```java
Tinify.setKey("YOUR_API_KEY");
Tinify.fromFile("unoptimized.png").toFile("optimized.png");
```
也就是说,我们只需要配置 API_KEY 就好了,文件的输入路径和输出路径肯定就是 res 文件夹下的 drawable 或 mipmap 了。不过我们肯定想知道,压缩前后的图片的大小差距,这里可以通过生成 Json 文件来查看。
需要注意的是,如果你想把这个 Plugin 上传到仓库给别人使用,那肯定不能在工程里面写死 API_KEY,这个很好理解,也就是说我们需要用户可以动态配置 API_KEY。这就要牵扯到 Extensions 了。
#### Extensions
Extensions 即自定义配置项,这是什么呢?我们可以参考 app 模块的 build.gradle 文件:
```java
android {
compileSdkVersion 28
defaultConfig {
applicationId "top.omooo.pluginproject"
minSdkVersion 23
targetSdkVersion 28
//...
}
//...
}
```
如何把 compileSdkVersion 和 applicationId 的值解析出来呢?很简单,我们可以直接在 app 模块的 build.gradle 文件添加以下代码(即第一篇文章的第一种方式):
```groovy
class MyPlugin implements Plugin<Project> {
@Override
void apply(Project target) {
target.task("testExtensions") << {
def android = target['android']
println(android.compileSdkVersion)
def defaultConfig = android['defaultConfig']
println(defaultConfig.applicationId)
}
}
}
apply plugin: MyPlugin
```
然后执行 Task 就可以看到输出了。
熟悉了以上操作,这时候我们就可以确定我们的自定义配置项为以下结构:
```groovy
tinyInfo {
//资源目录
resourceDir = [
"app/src/main/res",
"other_module/src/main/res"
]
resourcePattern = [
"drawable[a-z-]*",
"mipmap[a-z-]*"
]
whiteList = [
]
apiKey = "******"
}
```
我们可以为以上的结构写一个 Bean 类,命名为 TinyPngExtension.groovy:
```java
class TinyPngExtension {
ArrayList<String> resourceDir
ArrayList<String> resourcePattern
ArrayList<String> whiteList
String apiKey
TinyPngExtension() {
resourceDir = []
resourcePattern = []
whiteList = []
apiKey = null
}
@Override
public String toString() {
return "TinyPngExtension.groovy{" +
"resourceDir=" + resourceDir +
", resourcePattern=" + resourcePattern +
", whiteList=" + whiteList +
", apiKey='" + apiKey + '\'' +
'}'
}
}
```
需要注意的是,这里的**变量名一定要和配置项的字段名一致**。
这个 Plugin 的大部分代码都在 Task,所以我们把这个 Task ( TinyPngTask.groovy ) 抽出来:
```java
import org.gradle.api.DefaultTask
import org.gradle.api.tasks.TaskAction
class TinyPngTask extends DefaultTask {
TinyPngExtension mTinyPngExtension
TinyPngTask() {
mTinyPngExtension = project.tinyInfo
}
@TaskAction
void run() {
println("Task run~")
println(mTinyPngExtension.toString())
}
}
```
注意,这里我们用了一个 @TaskAction 的注解,这个注解表示该方法表示为 Task 的执行实体,方法名随意写都行。
然后我们在 Plugin 里去执行这个 Task ( CrazyPlugin.groovy ):
```groovy
class CrazyPlugin implements Plugin<Project> {
@Override
void apply(Project project) {
project.extensions.create("tinyInfo", TinyPngExtension)
project.afterEvaluate {
project.task("tinyTask", type: TinyPngTask)
}
}
}
```
回顾上一篇文章,我们说到,既然我们修改了 Plugin 的代码,就要重新生成 jar 包上传依赖,即执行:
```
./gradlew task uploadArchives
```
然后在 app 模块的 build.gradle 文件里,我们就可以 apply Plugin 并且添加 Extensions 了:
```groovy
apply plugin: 'top.omooo.crazy_plugin'
tinyInfo {
//资源目录
resourceDir = [
"app/src/main/res",
"other_module/src/main/res"
]
resourcePattern = [
"drawable[a-z-]*",
"mipmap[a-z-]*"
]
whiteList = [
]
apiKey = "******"
}
```
然后我们在执行:
```
./gradlew task tinyTask
```
就可以输出我们的自定义配置项了。也就是说,我们写的自定义配置项可以在 Task 中获取的。
#### Json 解析与生成
其实本来我是不想讲这个的,主要是因为太简单了,但是不讲这个好像没什么可写的了 :)
```java
class MyPlugin implements Plugin<Project> {
@Override
void apply(Project target) {
target.task("testJson") << {
def jsonFile = new File("${project.projectDir}/test.json")
if (!jsonFile.exists()) {
jsonFile.createNewFile()
}
//把 List 写入 json 文件中
ArrayList<String> list = ["Omooo", "Tom", "Test"]
def jsonOutput = new JsonOutput()
def json = jsonOutput.toJson(list)
jsonFile.write(jsonOutput.prettyPrint(json), "utf-8")
//把 json 文件读成 List
def readList = new JsonSlurper().parse(jsonFile, "utf-8")
println("${readList.toString()}")
}
}
}
apply plugin: MyPlugin
```
完全不用我过多解释了,大家都能看懂。但是这个有什么用嘛?
我们会通过配置项中拿到资源目录,然后遍历资源目录,批量压缩目录下的所有 png 图片,这时候我想把压缩前后的图片大小记录下来,就要用到写 Json 文件;下次压缩的时候,之前压缩过的图片,肯定不希望再次压缩了,所以就需要用到读 Json 文件转成 List,在这 List 的里面的图片就不用压缩了。
#### 结果
这是压缩后的结果 Json 文件:
```xml
[
{
"preSize": "6.73KB",
"md5": "cebfdfd96b51a2bbde7cfa40b7ea2997",
"postSize": "3.48KB",
"path": "app/src/main/res/mipmap-xhdpi/ic_launcher_round.png"
},
//...
{
"preSize": "74.46KB",
"md5": "d506a51ab02f8e527b2d5b13b24ba9f7",
"postSize": "16.30KB",
"path": "app/src/main/res/drawable/thread_lifecycle.png"
}
]
```
```
//*************//
Task finish,compress 11 files
Before total size: 141400
After total size: 49327
//*************//
```
效果还是很明显的~
#### 代码
[TinyPngPlugin](https://github.com/surpriseprojects/TinyPngPlugin)
本文无限感谢 [https://github.com/waynell/TinyPngPlugin](https://github.com/waynell/TinyPngPlugin)
其实代码并没有太大差别,毕竟代码就那些。
还记得自己当初 fork 这个库的时候,对于自定义 Plugin 流程不熟悉,还有就是不熟悉 Groovy 语法等一系列原因,一直没有去自己写一遍。当你熟悉了自定义 Plugin 流程后,Groovy 语法并不多,主要的我都讲到了,其实整个流程还是很简单的,还是开头那句话:
放心大胆的去 Fork 吧~

@ -36,4 +36,5 @@ ClassFile{
5. interfaces_count 和 interfaces,这两个成员表示该类实现了多少个接口以及接口类的类名。和 this_class 一样,这两个成员也只是常量池数组里的索引号。真正的信息需要通过解析常量池的内容才能得到。
6. fields_count 和 fields 包含了成员变量的数量以及它们的信息,成员变量信息由 field_info 结构体表示。
7. methods_count 和 methods 包含了成员函数的数量以及它们的信息,成员函数信息由 method_info 结构体表示。
8. attributes_count 和 attributes 包含了属性信息。属性信息由 attributes_info 结构体表示。属性包含哪些信息呢?比如调试信息就记录了某句代码对应源文件哪一行、函数对应的 Java 字节码也属于属性信息的一种。另外,源文件中的注解也属于注解。
8. attributes_count 和 attributes 包含了属性信息。属性信息由 attributes_info 结构体表示。属性包含哪些信息呢?比如调试信息就记录了某句代码对应源文件哪一行、函数对应的 Java 字节码也属于属性信息的一种。另外,源文件中的注解也属于属性。

@ -0,0 +1,137 @@
---
常量池及相关内容
---
#### 目录
1. 常量项的类型和关系
2. 信息描述规则
#### 常量项的类型和关系
在 JVM 规范中,常量池的英文叫 Constant Pool,对应的数据结构就是一个类型为 cp_info 的数组,每一个 cp_info 对象存储了一个常量项。cp_info 对应数据结构的伪代码如下:
```java
cp_info{
u1 tag; //常量项的类型
u1 info[]; //常量项的内容
}
```
由伪代码可知,每一个常量项的第一个字节用于表示常量项的类型,紧接其后的才是具体的常量项内容。那么,常量项会有哪些类型呢?
| 常量项类型 | tag 取值 | 含义 |
| --------------------------- | -------- | ------------------------------------------------------------ |
| CONSTANT_Class | 7 | 代表类或接口的信息 |
| CONSTANT_Fieldref | 9 | 同下 |
| CONSTANT_Methodref | 10 | 这三种常量项有相似的内容,分别存储成员变量、成员函数和接口函数信息。这些信息包括所属类的类名、变量和函数名、函数参数、返回值类型等 |
| CONSTANT_InterfaceMethodref | 11 | 同上 |
| CONSTANT_String | 8 | 代表一个字符串(String)。注意,该常量项本身不存储字符串的内容,它只存储了一个索引值 |
| CONSTANT_Integer | 3 | Java 中,int 和 float 型数据的长度都是 4 个字节。这两种常量项分别代表 int 和 float 型数据的信息 |
| CONSTANT_Float | 4 | 同上 |
| CONSTANT_Long | 5 | Java 中,long 和 double 型数据的长度都是 8 个字节。这两种常量项分别代表 long 和 double 型数据的信息 |
| CONSTANT_Double | 6 | 同上 |
| CONSTANT_NameAndType | 12 | 这种类型的常量项用于描述类的成员域或成员函数相关的信息 |
| CONSTANT_Utf8 | 1 | 用于存储字符串的常量项。注意,该项真正包含了字符串的内容。而 CONSTANT_String 常量项只存储了一个指向 CONSTANT_Utf8 项的索引 |
| CONSTANT_MethodHandle | 15 | 用于描述 MethodHandle 信息。MethodHandle 和反射有关系。Java 类库中对应的类为 java.lang.invoke.MethodHandle |
| CONSTANT_MethodType | 16 | 用于描述一个成员函数的信息,只包括函数的参数类型和返回值类型,不包括函数名和所属类的类名 |
| CONSTANT_InvokeDynamic | 18 | 用于 invokeDynamic 指令。invokeDynamic 和 Java 平台上实现了一些动态语言(如 Python、Ruby)相类似的有关功能 |
##### CONSTANT_String 和 CONSTANT_Utf8 的区别
CONSTANT_Utf8:
该常量项真正存储了字符串的内容。下面我们将看到此类型常量项对应的数据结构中有一个字节数组,字符串就存储在这个字节数组中。
CONSTANT_String:
代表一个字符串,但是它本身不包含字符串的内容,而仅仅包含一个指向类型为 CONSTANT_Utf8 常量项的索引。
下面我们看看几种常见常量项的内容,如下:
```xml
CONSTANT_Utf8_info{
u1 tag;
u2 length;
u1 bytes[length];
}
CONSTANT_Class_info{
u1 tag;
u2 name_index;
}
CONSTANT_Fieldref_info{
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
CONSTANT_String_info{
u1 tag;
u2 string_index;
}
CONSTANT_MethodType_info{
u1 tag;
u2 descriptor_index;
}
CONSTANT_Methodref_info{
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
CONSTANT_NameAndType_info{
u1 tag;
u2 name_index;
u2 descriptor_index;
}
CONSTANT_InterfaceMethodref_info{
u1 tag;
u2 class_index;
u2 name_and_type_index;
}
```
首先看 CONSTANT_Utf8_info,其中 length 表示 bytes 数组的长度,而 bytes 成员则真正存储字符串的内容。了解这一点信息很重要,因为凡是需要表示字符串的地方实际上都是指向常量池中一个类型为 CONSTANT_Utf8_info 元素的索引。
比如 Class_info 中的 name_index、String_info 中的 string_index、MethodType_info 中的 descriptor_index 等等,都代表一个指向类型为 CONSTANT_Utf8_info 元素的索引。
我们在看下基本数据类型常量项对应的数据结构:
```
CONSTANT_Long_info{
u1 tag;
u4 high_bytes;
u4 low_bytes;
}
CONSTANT_Integer_info{
u1 tag;
u4 bytes;
}
CONSTANT_Double_info{
u1 tag;
u4 high_bytes;
u4 low_bytes;
}
CONSTANT_Float_info{
u1 tag;
u4 bytes;
}
```
我们可以看到以上结构体内直接就能存储数据,这几个 info 之间没有引用关系。
对于前面的 info 而言,不在每个常量项里直接包含字符串信息,而是采用间接引用元素索引的方式,这是**为了节省 Class 文件的空间**,很好理解就不用多说了。
**除了采用引用索引的方式以节省空间外,规范对用于描述成员变量、成员函数相关的字符串的格式也有要求。**
#### 信息描述规则
根据 Java 虚拟机规范,如何用字符串来描述成员变量、成员函数是有讲究的,
Loading…
Cancel
Save