2024年3月

写在开头

面试官:“小伙子,java的集合学过吗?”
我:“肯定学过呀!”,这时候的我自信满满,手撕集合八股文嘛,早已背的滚瓜烂熟了呀。
面试官:“那你来讲讲集合使用时,应该注意哪些问题吧”
我:“额,这,我想想哈。”,什么!这面试官不按套路出牌,上来就问注意事项,打我一个措手不及啊。
我:“嗯 ~,我觉得应该注意该注意的问题!”
面试官:“下一位!”

集合使用注意事项

经过了十几篇博客的总结,java集合部分的知识点,大致上就学完了,当然,Collection与Map拥有着大量的子集,我们无法通过短短的五六万字就可以全部讲解完,后续会持续性的完善,现阶段呢,我们就先讲那么多哈。
今天,我们结合《阿里巴巴 Java 开发手册》,来对集合日常开发使用过程中的注意事项进行总结,大致可以分为以下几点。

集合判空

判空是集合在使用时必须要做的操作,我们得保证我们所创建的,或者所调用的别人创建的集合对象可用(不为null,不为空),才能进行下一步业务逻辑的开发。
那么,如何进行判空处理呢?我们这里以ArrayList为例,去列举一下它的判空处理方式。

【代码示例1】

public class Test {
    public static void main(String[] args) {
        ArrayList<Integer> list = new ArrayList<>();
        list.add(1);
        list.add(2);
        list.add(3);
        //方式一,list != null && !list.isEmpty()
        if (list != null && !list.isEmpty()) {
            for (Integer integer : list) {
                System.out.println("方式1:"+list);
            }
        } else {
            System.out.println("ArrayList读取异常!");
        }
        //方式二,list != null && list.size() > 0
        if (list != null && list.size() > 0) {
            for (Integer integer : list) {
                System.out.println("方式2:"+list);
            }
        } else {
            System.out.println("ArrayList读取异常!");
        }
        //方式三,org.apache.commons.collections包下的 CollectionUtils工具类
        if (CollectionUtils.isNotEmpty(list)) {
            for (Integer integer : list) {
                System.out.println("方式2:"+list);
            }
        } else {
            System.out.println("ArrayList读取异常!");
        }
    }
}

我们在这里列举了3种判空方式,那这3种方式之间又有何区别呢?让俺来分析一波。

第一点:
我们要知道null与空的区别,这是两个概念,很多初学者会混淆,为null表示这个list还没有分配内存,也就在堆中不存在,而空表示list的初始化工作已经完成,只不过里面没有任何元素。
我们在判空的时候需要注意,
!=null
要放在&&逻辑与的前面判断,因为,我们首先要保证list的初始化完成,才能去判断集合元素的是否存在,否则会报nullException。
第二点:
list.isEmpty() 与 list.size() == 0功能实现上一致,但在《阿里巴巴 Java 开发手册》中指出:

判断所有集合内部的元素是否为空,使用 isEmpty() 方法,而不是 size()==0 的方式

这是因为 isEmpty() 方法的可读性更好,并且时间复杂度为 O(1)。绝大部分我们使用的集合的 size() 方法的时间复杂度也是 O(1),不过,也有很多复杂度不是 O(1) 的,比如 java.util.concurrent 包下的某些集合(ConcurrentLinkedQueue、ConcurrentHashMap)。
以ConcurrentHashMap为例,我们可以看一下它底层关于size()与isEmpty()的实现

【源码解析1】

public int size() {
    long n = sumCount();
    return ((n < 0L) ? 0 :
            (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
            (int)n);
}
final long sumCount() {
    CounterCell[] as = counterCells; CounterCell a;
    long sum = baseCount;
    if (as != null) {
        for (int i = 0; i < as.length; ++i) {
            if ((a = as[i]) != null)
                sum += a.value;
        }
    }
    return sum;
}
public boolean isEmpty() {
    return sumCount() <= 0L; // ignore transient negative values
}

集合去重

很多场景下,我们都要求数据的唯一性,也就是不可重复,所以集合的去重本领我们也要掌握,在《阿里巴巴 Java 开发手册》中这样说道:

可以利用 Set 元素唯一的特性,可以快速对一个集合进行去重操作,避免使用 List 的 contains()
进行遍历去重或者判断包含操作。

这是为什么呢?我们依旧需要透过源码去分析问题,分别选择HashSet和ArrayList,其实两者的差别主要体现在对contains()的实现上。

【HashSet去重核心】

private transient HashMap<E,Object> map;
public boolean contains(Object o) {
    return map.containsKey(o);
}

HashSet 的 contains() 方法底部依赖的 HashMap 的 containsKey() 方法,时间复杂度接近于 O(1)(没哈希冲突下)。

【ArrayList去重核心】

public boolean contains(Object o) {
    return indexOf(o) >= 0;
}
public int indexOf(Object o) {
    if (o == null) {
        for (int i = 0; i < size; i++)
            if (elementData[i]==null)
                return i;
    } else {
        for (int i = 0; i < size; i++)
            if (o.equals(elementData[i]))
                return i;
    }
    return -1;
}

而对于ArrayList来说,它的contains是通过遍历元素实现,时间复杂度O(n),两者一比,高下立现!

集合遍历

集合元素的遍历,可以说是只要用集合,就无法避免的,之前写了一篇关于HashMap的遍历,还有一篇关于java中迭代器的文章,推荐大家去看看。
《HashMap的7种遍历方式》
《java中的迭代器实现原理》

不过对于集合遍历,在手册中有个额外的规约

不要在 foreach 循环里进行元素的 remove/add 操作。remove 元素请使用 Iterator 方式,如果并发操作,需要对Iterator 对象加锁。

强行修改,会导致Iterator遍历出错,报ConcurrentModificationException异常。

集合转数组

对于集合转为数组的场景,《阿里巴巴 Java 开发手册》也给了要求,如下:

使用集合转数组的方法,必须使用集合的 toArray(T[] array),传入的是类型完全一致、长度为 0 的空数组。

【代码示例2】

String [] s= new String[]{
    "I Love", "JavaBuild"
};
List<String> list = Arrays.asList(s);
Collections.reverse(list);
//没有指定类型的话会报错
s=list.toArray(new String[0]);

注意:new String[0]就是起一个模板的作用,指定了返回数组的类型,0 是为了节省空间,因为它只是为了说明返回的类型。

集合转Map

集合除了会转为数组外,还可能会转为Map,所以,我们在转Map的时候,《阿里巴巴 Java 开发手册》也给了约束。

在使用 java.util.stream.Collectors 类的 toMap() 方法转为 Map 集合时,一定要注意当 value 为
null 时会抛 NPE 异常。

class Person {
    private String name;
    private String phoneNumber;
     // getters and setters
}
//test main()
List<Person> bookList = new ArrayList<>();
bookList.add(new Person("1","JavaBuild"));
bookList.add(new Person("2",null));
// 空指针异常
bookList.stream().collect(Collectors.toMap(Person::getName, Person::getPhoneNumber));

这是为啥呢,我们跟入toMap中发现,内部调用了Map的merge()方法,跟入这个方法后,我们会发现

【源码解析】

default V merge(K key, V value,
        BiFunction<? super V, ? super V, ? extends V> remappingFunction) {
    Objects.requireNonNull(remappingFunction);
    Objects.requireNonNull(value);
    V oldValue = get(key);
    V newValue = (oldValue == null) ? value :
               remappingFunction.apply(oldValue, value);
    if(newValue == null) {
        remove(key);
    } else {
        put(key, newValue);
    }
    return newValue;
}

这源码里首先执行了 Objects.requireNonNull(remappingFunction);这一句代码,用来判断value值非空,并且做了抛出NPE处理。

总结

以上就是结合开发手册和自己平时开发经验,写的六点注意事项,希望所有小伙伴都能够在日后的开发工作中,保持良好的开发规范与习惯,强烈建议每个人必看《阿里巴巴 Java 开发手册》,这是很多互联网企业,新员工入职必看书籍,虽然里面有些内容,个人感觉有点矫枉过正,但90%以上的约定都非常必要!

结尾彩蛋

如果本篇博客对您有一定的帮助,大家记得
留言+点赞+收藏
呀。原创不易,转载请联系Build哥!

如果您想与Build哥的关系更近一步,还可以关注俺滴公众号“JavaBuild888”,在这里除了看到《Java成长计划》系列博文,还有提升工作效率的小笔记、读书心得、大厂面经、人生感悟等等,欢迎您的加入!

最近碰到一个 case,值得分享一下。

现象就是一个 update 操作,在 mysql 客户端中执行提示 warning,但在 java 程序中执行却又报错。

问题重现

mysql> create table test.t1(id int primary key, c1 datetime);
Query OK, 0 rows affected (0.01 sec)

mysql> insert into test.t1 values(1,now());
Query OK, 1 row affected (0.00 sec)

mysql> update test.t1 set c1=str_to_date('2024-02-23 01:01:01.0','%Y-%m-%d %H:%i:%s') where id=1;
Query OK, 1 row affected, 1 warning (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 1

mysql> show warnings;
+---------+------+-------------------------------------------------------------+
| Level   | Code | Message                                                     |
+---------+------+-------------------------------------------------------------+
| Warning | 1292 | Truncated incorrect datetime value: '2024-02-23 01:01:01.0' |
+---------+------+-------------------------------------------------------------+
1 row in set (0.00 sec)

mysql> select * from test.t1;
+----+---------------------+
| id | c1                  |
+----+---------------------+
|  1 | 2024-02-23 01:01:01 |
+----+---------------------+
1 row in set (0.00 sec)

update 语句中使用
STR_TO_DATE
函数将字符串转换为日期时间格式。

但因为这个格式字符串
'%Y-%m-%d %H:%i:%s'
没有对日期字符串中的毫秒部分
.0
进行解析,所以这一部分会被 truncate 掉。

可以看到,该语句在 mysql 客户端中执行时没有报错,只是提示 warning。

同样的 SQL,在下面这段 java 代码中跑却直接报错。

package com.example;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;

public class JdbcTest {

    private static final String JDBC_URL = "jdbc:mysql://10.0.0.198:3306/information_schema";
    private static final String USER = "root";
    private static final String PASSWORD = "123456";

    public static void main(String[] args) {
        try (Connection connection = DriverManager.getConnection(JDBC_URL, USER, PASSWORD)) {
            try (Statement statement = connection.createStatement()) {
                String updateQuery = "UPDATE test.t1 SET c1 = STR_TO_DATE('2024-02-23 01:01:01.0', '%Y-%m-%d %H:%i:%s') WHERE id=1";
                int rowsAffected = statement.executeUpdate(updateQuery);
                System.out.println("Rows affected: " + rowsAffected);
            }
        } catch (SQLException e) {
            e.printStackTrace();
        }
    }
}
# java -jar target/jdbc-test-1.0-SNAPSHOT-jar-with-dependencies.jar
com.mysql.cj.jdbc.exceptions.MysqlDataTruncation: Data truncation: Truncated incorrect datetime value: '2024-02-23 01:01:01.0'
        at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:104)
        at com.mysql.cj.jdbc.StatementImpl.executeUpdateInternal(StatementImpl.java:1337)
        at com.mysql.cj.jdbc.StatementImpl.executeLargeUpdate(StatementImpl.java:2112)
        at com.mysql.cj.jdbc.StatementImpl.executeUpdate(StatementImpl.java:1247)
        at com.example.JdbcTest.main(JdbcTest.java:17)

问题根因

刚开始以为这个报错跟 sql_mode 有关,但实际上这个实例的 sql_mode 为空。

mysql> show global variables like '%sql_mode%';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| sql_mode      |       |
+---------------+-------+
1 row in set (0.00 sec)

所以,一开始就排除了 sql_mode 的可能性。

但万万没想到,JDBC 驱动会偷偷修改 sql_mode 的会话值。

在上面的 java 程序中加了一段代码,用来打印 sql_mode 的会话值。

ResultSet resultSet = statement.executeQuery("SELECT @@SESSION.sql_mode");
if (resultSet.next()) {
    String sqlModeValue = resultSet.getString(1);
    System.out.println("Current sql_mode value: " + sqlModeValue);
}

结果发现当前会话的 sql_mode 竟然是
STRICT_TRANS_TABLES

Current sql_mode value: STRICT_TRANS_TABLES


STRICT_TRANS_TABLES
就是导致 update 操作报错的罪魁祸首!

这一点,很容易在 mysql 客户端中验证出来。

mysql> set session sql_mode='STRICT_TRANS_TABLES';
Query OK, 0 rows affected, 1 warning (0.00 sec)

mysql> update test.t1 set c1=str_to_date('2024-02-23 01:01:01.0','%Y-%m-%d %H:%i:%s') where id=1;
ERROR 1292 (22007): Truncated incorrect datetime value: '2024-02-23 01:01:01.0'

所以,问题来了, sql_mode 是在哪里修改的?

sql_mode 是在哪里修改的?

分析 JDBC 驱动代码,发现会话的 sql_mode 是在
setupServerForTruncationChecks
中修改的。

该方法是在连接建立后,初始化时调用的。

其主要作用是检查当前会话的 sql_mode 是否包含
STRICT_TRANS_TABLES
,如果不包含,则会通过
SET
命令修改当前会话的 sql_mode,使其包含
STRICT_TRANS_TABLES

// src/main/user-impl/java/com/mysql/cj/jdbc/ConnectionImpl.java
private void setupServerForTruncationChecks() throws SQLException {
    synchronized (getConnectionMutex()) {
        // 获取 JDBC 驱动程序配置中的 jdbcCompliantTruncation 属性
        RuntimeProperty<Boolean> jdbcCompliantTruncation = this.propertySet.getProperty(PropertyKey.jdbcCompliantTruncation);
        if (jdbcCompliantTruncation.getValue()) {
            // 获取当前会话的 sql_mode
            String currentSqlMode = this.session.getServerSession().getServerVariable("sql_mode");
            // 检查 sql_mode 中是否包含 STRICT_TRANS_TABLES 选项
            boolean strictTransTablesIsSet = StringUtils.indexOfIgnoreCase(currentSqlMode, "STRICT_TRANS_TABLES") != -1;
            // 如果 sql_mode 为空,或长度为 0,或不包含 STRICT_TRANS_TABLES 选项,
            // 则构建 SET sql_mode 语句,将 STRICT_TRANS_TABLES 添加到 sql_mode 中
            if (currentSqlMode == null || currentSqlMode.length() == 0 || !strictTransTablesIsSet) {
                StringBuilder commandBuf = new StringBuilder("SET sql_mode='");

                if (currentSqlMode != null && currentSqlMode.length() > 0) {
                    commandBuf.append(currentSqlMode);
                    commandBuf.append(",");
                }
     
                commandBuf.append("STRICT_TRANS_TABLES'");
                // 执行 SET sql_mode 语句
                this.session.execSQL(null, commandBuf.toString(), -1, null, false, this.nullStatementResultSetFactory, null, false);

                jdbcCompliantTruncation.setValue(false); // server's handling this for us now
            } else if (strictTransTablesIsSet) {
                // 如果 sql_mode 中包含 STRICT_TRANS_TABLES 选项,则不做任何调整
                // We didn't set it, but someone did, so we piggy back on it
                jdbcCompliantTruncation.setValue(false); // server's handling this for us now
            }
        }
    }
}

所以,尽管 mysql 服务端的 sql_mode 为空,但由于 JDBC 驱动将会话的 sql_mode 调整为了
STRICT_TRANS_TABLES
,最后还是导致 update 操作报错。

如何解决 java 程序中执行报错的问题

很简单,在 JDBC URL 中将
jdbcCompliantTruncation
属性设置为 false。

jdbc:mysql://10.0.0.198:3306/information_schema?jdbcCompliantTruncation=false

除此之外,也可修改 java 代码,在 update 操作之前显式设置 sql_mode 的会话值,如,

statement.execute("SET @@SESSION.sql_mode = ''");
String updateQuery = "UPDATE test.t1 SET c1 = STR_TO_DATE('2024-02-23 01:01:01.0', '%Y-%m-%d %H:%i:%s') WHERE id=1";

但这种方式对应用代码有侵入,不建议这么做。

实际上,JDBC 驱动支持在 URL 中修改参数的会话值。

在 URL 中修改参数的会话值,有以下好处:

  • 无需在每次 SQL 操作之前显式执行设置语句。这使得配置变更更为集中化,更容易管理和维护。

  • 避免了对应用代码的直接侵入,提高了代码的可维护性和灵活性。

JDBC 驱动中如何修改参数的会话值

从 mysql-connector-java 3.1.8 开始,支持通过
sessionVariables
属性修改 MySQL 参数的会话值。语法如下:

sessionVariables=variable_name1=variable_value1,variable_name1=variable_value2...variable_nameN=variable_valueN

多个参数之间使用逗号或者分号隔开。

看下面这个示例,同时修改 explicit_defaults_for_timestamp,group_concat_max_len 和 sql_mode 的会话值。

JDBC_URL = "jdbc:mysql://10.0.0.198:3306/information_schema?sessionVariables=explicit_defaults_for_timestamp=OFF,group_concat_max_len=2048,sql_mode='NO_ZERO_IN_DATE,NO_ZERO_DATE'"

注意,如果
jdbcCompliantTruncation
为 true(默认值),即使
sessionVariables
中设置的 sql_mode 不包含
STRICT_TRANS_TABLES
,最终生效的 sql_mode 的会话值还是会包含
STRICT_TRANS_TABLES

之所以会这样,主要是因为
sessionVariables
的设置先于
setupServerForTruncationChecks

JDBC 驱动为什么要修改 sql_mode 的会话值

这个实际上是 JDBC 规范的要求。

Connector/J issues warnings or throws
DataTruncation
exceptions as is required by the JDBC specification, unless the connection was configured not to do so by using the property
jdbcCompliantTruncation
and setting it to
false
.

参考资料

  1. https://docs.oracle.com/cd/E17952_01/connector-j-8.0-en/connector-j-reference-type-conversions.html
  2. https://dev.mysql.com/doc/connector-j/en/connector-j-connp-props-session.html

亲爱的读者朋友们,今天我要给大家介绍一个强大的开源工具——
AntSK
。这个工具能让您在没有Internet连接时依然能使用人工智能知识库对话和查询,想象一下,即使在无网络的环境中,您也能与AI进行愉快的交流!

项目地址:

https://github.com/xuzeyu91/AntSK

AntSK不仅免费、开源,更妙的是它可以完全离线运行,不再依赖于OpenAI的API接口。它的核心是集成了
LLamaSharp
,让我们可以在本地机器上运行gguf格式的模型。本次教程中,我们使用的是“
tinyllama-1.1b-chat.gguf
”模型来进行测试。

初探AntSK的配置世界

配置可能是很多人的噩梦,但AntSK的配置简单明了。首先,看到配置文件时,我们主要关注以下几个要点:

"LLamaSharp": {
  "Chat": "D:\\Code\\AI\\AntBlazor\\model\\tinyllama-1.1b-chat.gguf",
  "Embedding": "D:\\Code\\AI\\AntBlazor\\model\\tinyllama-1.1b-chat.gguf"
},

在这里,我的
Embedding

Chat
都是配置成相同的
tinyllama
模型。你可能会问,为什么需要两个相同的路径?这是因为AntSK提供了聊天和语义嵌入两种功能,二者可以使用同一个模型,也可以分别配置不同的模型以满足不同的需求。

下一步,我们要模拟设置OpenAI的终端:

"OpenAIOption": {
  "EndPoint": "https://localhost:5001/llama/",
  "Key": "这个用本地可以随便写",
  "Model": "这个用本地可以随便写",
  "EmbeddingModel": "这个用本地可以随便写"
},

在实际本地运行时,上述的“EndPoint”将是服务的本地地址,而其他的Key、Model等字段在本地运行时并不会被实际验证,所以你可以随意填写。

激动人心的第一次启动

当我们配置好所有必要的参数之后,就可以启动程序,看看它第一次呼吸的样子了:

你看,AI已经在本地运行起来了,一切准备就绪后,界面会展示给我们一个期待已久的聊天窗口。

知识库文档的导入与应用

AntSK支持导入知识库文档,这意味着你可以将专业知识、文档或是你日常积累的笔记嵌入到系统中:

有了这些定制化的文档后,AntSK的AI能针对特定的内容给出更为精准的反馈和答案。

实战测试:和AI进行问答

终于到了最激动人心的时刻——我对AntSK进行了一系列的问答测试,看看它的表现如何:

备注:这个是离线运行效果,不依赖LLMAPI而是直接本地加载模型,并且我使用的是CPU进行的知识库文档导入和问答

从测试结果来看,AntSK回答问题流畅、准确,给人近乎实时聊天的体验。可以想象,在没有网络的情况下,这样的工具将极大地方便我们的生活和工作。

加入我们的交流群,一起成长

学习新技术的路上,交流总是非常重要的。我在这里诚邀各位感兴趣的朋友,加入我们的交流群。在这里,你可以和如我一样热爱.NET和AI技术的小伙伴们交流心得,分享经验,互相解答疑惑。

最后,如果你对AntSK或LLamaSharp有任何疑问或想要了解更多细节,不妨访问AntSK的GitHub项目页面,那里有更详细的文档和指引。

亲爱的朋友们,感谢你们阅读这篇文章,希望对你们了解和使用AntSK带来帮助。我将继续关注.NET/AI技术领域的最新发展,为大家带来更多优质的内容。记得关注、点赞和分享哦!我们下次见!

本文基于Glide 4.11.0

Glide加载过程有一个解码过程,比如将url加载为inputStream后,要将inputStream解码为Bitmap。


Glide源码解析一
我们大致知道了Glide加载的过程,所以我们可以直接从这里看起,在这个过程中我们以从文件中加载bitmap为例:

DecodeJob的一个方法:

private voiddecodeFromRetrievedData() {if(Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey(
"Retrieved data", startFetchTime,"data: " +currentData+ ", cache key: " +currentSourceKey+ ", fetcher: " +currentFetcher);
}
Resource
<R> resource = null;try{
resource
=decodeFromData(currentFetcher, currentData, currentDataSource);
}
catch(GlideException e) {
e.setLoggingDetails(currentAttemptingKey, currentDataSource);
throwables.add(e);
}
if (resource != null) {
notifyEncodeAndRelease(resource, currentDataSource);
}
else{
runGenerators();
}
}

主要是这个方法:resource = decodeFromData(currentFetcher, currentData, currentDataSource);

这时候currentData为FileInputStream,因为我们加载的是本地文件。

currentDateSource为LOCAL,即为本地的资源

我们继续找下去

resource = decodeFromData(currentFetcher, currentData, currentDataSource);

----------------->

Resource<R> result = decodeFromFetcher(data, dataSource);

------------------>

private <Data> Resource<R>decodeFromFetcher(Data data, DataSource dataSource)throwsGlideException {
LoadPath
<Data, ?, R> path = decodeHelper.getLoadPath((Class<Data>) data.getClass());return runLoadPath(data, dataSource, path);

这里获取到LoadPath的对象,我么先看看LoadPath有什么?

我们可以看到一个DecodePaths:

DecodePath里面又保存着decoders

decoders便是我们需要的解码器,拿到解码器后就可以进行解码了。

那怎么拿到?


Glide源码解析三
中我们知道这些解码器都注册在Register中,所以我们也是要通过它来拿:

<Data> LoadPath<Data, ?, Transcode> getLoadPath(Class<Data>dataClass) {returnglideContext.getRegistry().getLoadPath(dataClass, resourceClass, transcodeClass);
}

---------------->

@Nullablepublic <Data, TResource, Transcode> LoadPath<Data, TResource, Transcode>getLoadPath(
@NonNull Class
<Data> dataClass, @NonNull Class<TResource>resourceClass,
@NonNull Class
<Transcode>transcodeClass) {
LoadPath
<Data, TResource, Transcode> result =loadPathCache.get(dataClass, resourceClass, transcodeClass);if(loadPathCache.isEmptyLoadPath(result)) {return null;
}
else if (result == null) {
List
<DecodePath<Data, TResource, Transcode>> decodePaths =getDecodePaths(dataClass, resourceClass, transcodeClass);//It's possible there is no way to decode or transcode to the desired types from a given//data class. if(decodePaths.isEmpty()) {
result
= null;
}
else{
result
= new LoadPath<>(
dataClass, resourceClass, transcodeClass, decodePaths, throwableListPool);
}
loadPathCache.put(dataClass, resourceClass, transcodeClass, result);
}
returnresult;
}

首先会先从缓存中拿,缓存中拿不到再通过下面的方法去拿:

List<DecodePath<Data, TResource, Transcode>> decodePaths = getDecodePaths(dataClass, resourceClass, transcodeClass);

private <Data, TResource, Transcode> List<DecodePath<Data, TResource, Transcode>>getDecodePaths(
@NonNull Class
<Data> dataClass, @NonNull Class<TResource>resourceClass,
@NonNull Class
<Transcode>transcodeClass) {
List
<DecodePath<Data, TResource, Transcode>> decodePaths = new ArrayList<>();
List
<Class<TResource>> registeredResourceClasses =decoderRegistry.getResourceClasses(dataClass, resourceClass);for (Class<TResource>registeredResourceClass : registeredResourceClasses) {
List
<Class<Transcode>> registeredTranscodeClasses =transcoderRegistry.getTranscodeClasses(registeredResourceClass, transcodeClass);for (Class<Transcode>registeredTranscodeClass : registeredTranscodeClasses) {
List
<ResourceDecoder<Data, TResource>> decoders =decoderRegistry.getDecoders(dataClass, registeredResourceClass);
ResourceTranscoder
<TResource, Transcode> transcoder =transcoderRegistry.get(registeredResourceClass, registeredTranscodeClass);
@SuppressWarnings(
"PMD.AvoidInstantiatingObjectsInLoops")
DecodePath
<Data, TResource, Transcode> path = new DecodePath<>(dataClass, registeredResourceClass, registeredTranscodeClass,
decoders, transcoder, throwableListPool);
decodePaths.add(path);
}
}
returndecodePaths;
}

该方法各个参数如下:

dataClass为InputStream
,这是被解码的对象

resourceClass为Object
,要解码成为Object

transcodeClass为Drawable
,要转码为Drawable

我们看这个方法:

decoderRegistry.getResourceClasses:

public synchronized <T, R> List<Class<R>> getResourceClasses(@NonNull Class<T>dataClass,
@NonNull Class
<R>resourceClass) {
List
<Class<R>> result = new ArrayList<>();for(String bucket : bucketPriorityList) {
List
<Entry<?, ?>> entries =decoders.get(bucket);if (entries == null) {continue;
}
for (Entry<?, ?>entry : entries) {if(entry.handles(dataClass, resourceClass)&& !result.contains((Class<R>) entry.resourceClass)) {
result.add((Class
<R>) entry.resourceClass);
}
}
}
returnresult;
}

该方法是为了获取解码器中的resourceClass,即解码后的资源类型。

我们可以看到decoder这个map里面的内容:

各种类型对应的解码器。

只有满足entry.handles(dataClass, resourceClass),才能被添加返回:

public boolean handles(@NonNull Class<?> dataClass, @NonNull Class<?>resourceClass) {return this.dataClass.isAssignableFrom(dataClass) &&resourceClass
.isAssignableFrom(
this.resourceClass);
}

由于我们的resourceClass是Object,因此resourceClass .isAssignableFrom(this.resourceClass)总是成立的,所以就看:this.dataClass.isAssignableFrom(dataClass)

而我们的dataClass是InputStream,打开各种类型,可以看到哪些的dataClass是InputStream:

上面框错了,应该框resourceClass,另外FrameSequenceDrawable是我自定义后注册进去的,所以Glide原生的是没有的。

所以最终返回的resource为:

接下来是针对每一种resourceClass获取对应的转码类(要转成的对象):

public synchronized <Z, R> List<Class<R>>getTranscodeClasses(
@NonNull Class
<Z> resourceClass, @NonNull Class<R>transcodeClass) {
List
<Class<R>> transcodeClasses = new ArrayList<>();//GifDrawable -> Drawable is just the UnitTranscoder, as is GifDrawable -> GifDrawable. if(transcodeClass.isAssignableFrom(resourceClass)) {
transcodeClasses.add(transcodeClass);
returntranscodeClasses;
}
for (Entry<?, ?>entry : transcoders) {if(entry.handles(resourceClass, transcodeClass)) {
transcodeClasses.add(transcodeClass);
}
}
returntranscodeClasses;
}

如果transcodeClass是resourceClass的父类那就直接返回。

第一个GifDrawable,返回的registeredTranscodeClasses为:

然后根据dataClass, registeredResourceClass获取decoders:

然后根据registeredResourceClass和registeredTranscodeClass获取transcoder

上面具体的获取过程是类似的,就不过多分析了。

然后构造DecodePath,放进下面的集合里面:

List<DecodePath<Data, TResource, Transcode>> decodePaths = new ArrayList<>();

循环获取之后,最终得到的decodePaths如下:

大致流程:

1、先根据传进来的resourceClass获取注册表中所有注册的resourceClass得到List<Class<TResource>> registeredResourceClasses

2、两层for循环:

(1)外层:根据registeredResourceClasses获取转码的class :List<Class<Transcode>> registeredTranscodeClasses

(2)内层:

a、根据资源resourceClass获取所有的解码器。

b、根据资源resourceClass和转码transcodeClass获取所有的转码器。

c、构造DecodePath,放进集合里面。

最后得到的List<DecodePath<Data, TResource, Transcode>> decodePaths被放到LoadPath对象里面(上一层方法可看到)

我们又回到DecodeJob中的方法:

private <Data> Resource<R>decodeFromFetcher(Data data, DataSource dataSource)throwsGlideException {
LoadPath
<Data, ?, R> path = decodeHelper.getLoadPath((Class<Data>) data.getClass());returnrunLoadPath(data, dataSource, path);
}

获取到LoadPath后接下来就是要开始执行了runLoadPath了。

找下去可以看到该方法:

returnpath.load(
rewinder, options, width, height,
new DecodeCallback<ResourceType>(dataSource));

该方法属于LoadPath对象。

层层追溯后,最终来到下面的方法:

private Resource<Transcode> loadWithExceptionList(DataRewinder<Data>rewinder,
@NonNull Options options,
int width, int height, DecodePath.DecodeCallback<ResourceType>decodeCallback,
List
<Throwable> exceptions) throwsGlideException {
Resource
<Transcode> result = null;//noinspection ForLoopReplaceableByForEach to improve perf for (int i = 0, size = decodePaths.size(); i < size; i++) {
DecodePath
<Data, ResourceType, Transcode> path =decodePaths.get(i);try{
result
=path.decode(rewinder, width, height, options, decodeCallback);
}
catch(GlideException e) {
exceptions.add(e);
}
if (result != null) {break;
}
}
if (result == null) {throw new GlideException(failureMessage, new ArrayList<>(exceptions));
}
returnresult;
}

该方法在LoadPath里面,遍历decodePaths(这是我们之前获取后放在LoadPath中的)进行解码:

result = path.decode(rewinder, width, height, options, decodeCallback);

然后来到:

public Resource<Transcode> decode(DataRewinder<DataType> rewinder, int width, intheight,
@NonNull Options options, DecodeCallback
<ResourceType> callback) throwsGlideException {
Resource
<ResourceType> decoded =decodeResource(rewinder, width, height, options);
Resource
<ResourceType> transformed =callback.onResourceDecoded(decoded);returntranscoder.transcode(transformed, options);
}

我们这里需要看的就是:decodeResource:

最终来到DecodePath里面的方法:

@NonNullprivate Resource<ResourceType> decodeResourceWithList(DataRewinder<DataType> rewinder, intwidth,int height, @NonNull Options options, List<Throwable> exceptions) throwsGlideException {
Resource
<ResourceType> result = null;//noinspection ForLoopReplaceableByForEach to improve perf for (int i = 0, size = decoders.size(); i < size; i++) {
ResourceDecoder
<DataType, ResourceType> decoder =decoders.get(i);try{
DataType data
=rewinder.rewindAndGet();if(decoder.handles(data, options)) {
data
=rewinder.rewindAndGet();
result
=decoder.decode(data, width, height, options);
}
//Some decoders throw unexpectedly. If they do, we shouldn't fail the entire load path, but//instead log and continue. See #2406 for an example. } catch (IOException | RuntimeException |OutOfMemoryError e) {if(Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG,
"Failed to decode data for " +decoder, e);
}
exceptions.add(e);
}
if (result != null) {break;
}
}
if (result == null) {throw new GlideException(failureMessage, new ArrayList<>(exceptions));
}
returnresult;
}

这个方法:decoder.handles(data, options)是判断该解码器是否可以对该资源进行解码,这个方法写在每个解码器里面。

DataRewinder里面放着需要进行解码的数据。

解码后将资源返回。

又回到这个方法:

public Resource<Transcode> decode(DataRewinder<DataType> rewinder, int width, intheight,
@NonNull Options options, DecodeCallback
<ResourceType> callback) throwsGlideException {
Resource
<ResourceType> decoded =decodeResource(rewinder, width, height, options);
Resource
<ResourceType> transformed =callback.onResourceDecoded(decoded);returntranscoder.transcode(transformed, options);
}

这一句Resource<ResourceType> transformed = callback.onResourceDecoded(decoded);

是对资源进行变换处理,比如图片的缩放,剪裁等等,这个功能单独拎出来讲。

接下来便是运用转码器进行资源的转码:

transcoder.transcode(transformed, options)

到此就结束了。

转载请标明:
https://www.cnblogs.com/tangZH/p/12912698.html

前言

今天大姚给大家分享一款.NET开源的、功能强大的串口调试工具:LLCOM。

工具介绍

LLCOM是一个.NET开源的、功能强大的串口调试工具。支持Lua自动化处理、串口调试、串口监听、串口曲线、TCP测试、MQTT测试、编码转换、乱码恢复等功能。

功能列表

  • 收发日志清晰明了,可同时显示HEX值与实际字符串。
  • 自动保存串口与Lua脚本日志,并附带时间。
  • 串口断开后,如果再次连接,会自动重连。
  • 发送的数据可被用户自定义的Lua脚本提前处理。
  • 右侧快捷发送栏,快捷发送条目数量不限制。
  • 右侧快捷发送栏,支持10页数据,互相独立。
  • 可独立运行Lua脚本,并拥有定时器与协程任务特性。
  • 可选文字编码格式、可单独隐藏发送数据。
  • 终端功能,直接敲键盘发送数据(包含ctrl+字母键)。
  • 集成TCP、UDP、SSL测试服务端/客户端功能,并且支持IPV6。
  • 集成各种编码互转功能、集成乱码恢复功能、集成mqtt测试功能。
  • 集成串口监听功能,可监听其他软件的串口通信数据。

运行工具源代码

设置
llcom
为启动项目启动运行:

独立的Lua脚本自动处理串口收发

右侧的Lua脚本调试区域,可直接运行你写的串口测试脚本,如软件自带的:

--注册串口接收函数
uartReceive = function (data)
    log.info("uartReceive",data)
    sys.publish("UART",data)--发布消息
end

--新建任务,等待接收到消息再继续运行
sys.taskInit(function()
    while true do
        local _,udata = sys.waitUntil("UART")--等待消息
        log.info("task waitUntil",udata)
        local sendResult = apiSendUartData("ok!")--发送串口消息
        log.info("uart send",sendResult)
    end
end)

--新建任务,每休眠1000ms继续一次
sys.taskInit(function()
    while true do
        sys.wait(1000)--等待1000ms
        log.info("task wait",os.time())
    end
end)

--1000ms循环定时器
sys.timerLoopStart(log.info,1000,"timer test")

工具部分功能截图

项目源码地址



更多项目实用功能和特性欢迎前往项目开源地址查看