小毛的胡思乱想

凡走过,必留痕迹.

Android-Universal-Image-Loader源码快扫

| Comments

ImageLoaderConfiguration/ImageLoaderConfiguration.Builder学习

构造复杂对象的方式: Builder模式,用来创建ImageLoaderConfiguration对象,适用于链式写法。

常见结构如下:关键点: 私有构造(拷贝而非应用,避免build复写)、内部类(影响局部化,内聚好)、返回this(支持链式)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class A {
  private A(Builder builder) {
    // create A with builder's copy
  }

  public static class Builder{
     Builder buildStep1(){
        //...
        return this;
     }

     Builder buildStep2(){
        //...
        return this;
     }

     A build() {
        return new A(this);
     }
  }
}

ImageLoaderConfiguration支持的特性

1
2
3
-- 源码没什么养分,关注特性是如何表现在内部结构上。
-- 类层次的结构,能体会就体会,不体会就拉到。层次是渐进实现的,不能体会也没什么。
-- 如果有机会的话,可以在实践项目中调试进去学学。

如果想学习相关特性是怎么实现的,可以根据配置的去反推实现代码(搜索或调用关系):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
基本特性(可配置):
  基本控制:
    线程池大小 -- 不能太多
    线程优先级 -- 略低
    请求任务排队  -- 默认FIFO
    是否开启调试日志 -- DEBUG log
  多级缓存特性:
    内存缓存大小
    硬盘缓存大小,文件数量限制,文件名生成规则
    -- 关联硬盘图片处理器BitmapProcessor
  图片专用:
    缓存图最大宽高 --- 用于约束图片大小
    显示图的约束
    -- 关联下载器ImageDownloader(还通过networkDeniedDownloader/slowNetworkDownloader区分不同情况,这不就是Null Object模式么?)
    -- 关联解码器ImageDecoder

关于ImageLoader如何解决错乱问题

ImageLoader就是一个singleton实现,配合init+ImageLoaderConfiguration进行初始化,没什么说的。很常见的设计实现。

大多是helper method 关注displayImage/loadImage最终实现即可。这也是常见做法,便于使用。

问题: url – 关联的view,问题在于url请求是异步的,而view可能被重复利用。
这里用的ImageAware,看他们的实现类,有个ViewAware,就是一个view的包装,我想说的是WeakReference在android中很常用呀。 – 这句是废话

1
2
3
4
使用ImageAware占位
  图片宽高
  实际的View --可重用
  id -- 尼玛,这是解决问题的关键

见DisplayBitmapTask有个isViewWasReused倒出重点,从它的实现就可以判断完整的算法拉。

内部结构 cacheKeysForImageAwares map<ImageAware.id, memorycachekey(由URI + 宽高生成)>

不过ViewAware的id是用view的hashcode来指定的, NonViewAware的id是用url来指定的。
so, listview的时候,使用loadImage是可以避免错乱的,而用displayImage就呵呵拉。
判断方式如下:根据id可以找到目前的url和当前的url比较即可。知道是不是被复用了。

相对于原理上的image#setTag(url)然后比较的方式,更加透明,侵入性少。

再看看ImageLoaderEngine在任务管理、线程方面的处理

原以为这货应该比较好处理,图样图森破呀,毕竟它支持多种状态(暂停,恢复,关闭等)

先说一下几个AtomicBoolean的变量,主要就是判断状态的boolean拉,当然这货是线程安全的,其实用boolean也行,毕竟也是原子的(注意可见性问题即可)

1
2
3
private final AtomicBoolean paused = new AtomicBoolean(false);
private final AtomicBoolean networkDenied = new AtomicBoolean(false);
private final AtomicBoolean slowNetwork = new AtomicBoolean(false);`

另外,还有3个线程执行器,简单看看用途。至于为什么要这么多个,哥认为是这样的:

  • 在loader中走进displayImage之后,它重要检查一下内存中有木有(耗时小,无需线程)。
  • 如果没有才会扔给engine,engine首先检查一下硬盘上有木有(有io,所以走taskDistributor)。
  • 如果硬盘上有,走taskExecutorForCachedImages(同理有io)
  • 还是没有,走其他(maybe网络io)
  • 在请求较多,兼顾命中、不命中的情况,它选择了采用多级的Executor
1
2
3
private Executor taskExecutor; -- 木有在disk的情况,一般走网络
private Executor taskExecutorForCachedImages; -- disk的情况
private Executor taskDistributor;  -- 根据情况分发给上面2

还有2个map,第一个就是用来保存id和图片url的对应关系(简单是这么理解)
另外是用来控制不同view但有相同uri的并发请求。

1
2
3
private final Map<Integer, String> cacheKeysForImageAwares = Collections
      .synchronizedMap(new HashMap<Integer, String>());
private final Map<String, ReentrantLock> uriLocks = new WeakHashMap<String, ReentrantLock>();

不熟悉ReentrantLock的童鞋可参考 https://www.ibm.com/developerworks/cn/java/j-jtp10264/

LoadAndDisplayImageTask通过控制相同的uri持有同一个锁,这样执行的时候,后面的就会等待。
具体可以参考LoadAndDisplayImageTask的实现,不过用ReentrantLock需要特别注意写法,避免死锁。
不过我认为getLockForUri并没有同步,还是有存在相同url获取到不同lock的可能,不过这不影响功能,而且受限于并发大小也很难出现。

关于url的中文

网上有看到一些人说中文图片名会出错,可能是很久之前的版本吧。我这里要说的是,我认为这是个简单的问题,即使改源码也很容易处理。
不过即使源码有相关处理,通常也不会关注的,不过今天刚好有人问我一个中文路径的问题,所以我就关注了一下它怎么实现的。

首先看BaseImageDownloader是如何请求中文图片的。由于http只是支持ascii的url编码,所以必须要编码的,通常用utf-8,虽然这个没规定。貌似我们的基线没有考虑这个问题,应该是没有掉过坑。

1
2
3
4
5
6
7
protected HttpURLConnection createConnection(String url, Object extra) throws IOException {
  String encodedUrl = Uri.encode(url, ALLOWED_URI_CHARS);
  HttpURLConnection conn = (HttpURLConnection) new URL(encodedUrl).openConnection();
  conn.setConnectTimeout(connectTimeout);
  conn.setReadTimeout(readTimeout);
  return conn;
}

另外,还有一个容易出现中文问题的就是保存到硬盘的情况(android貌似不是什么大问题,如果是做server开发的话,就得特别注意)
带的实现有HashCodeFileNameGenerator(默认)和Md5FileNameGenerator,这2种处理都不会产生中文问题。
不过我建议还是用Md5FileNameGenerator,hashcode做唯一性并不是很靠谱,如果是大量的固定文件名长度的图片,还是很容易冲突的。

小结

源码何其多,带着问题学习效果更好。挑几个疑惑看看别人怎么处理就是收获。
从类的层次着手是很困难的,特别是大型源码。了解上层架构,学示例,然后调调源码或许更好。 大多数情况,相对于细节,应该更关注关键数据的结构、如何组织数据的结构来解决问题。
如果自己设计,应该考虑的重要问题有: 如何使用? 用怎样的结构表示数据和状态?
XX设计模式不要硬套,从过程式演变出来更加自然(经验性的除外)。推荐重构与模式。
大而全的源码解读没有什么用,带问题分析的更有价值。
学好基础,模仿起来也不容易掉坑。

– 以上纯属肉眼分辨,并无调试过,不做正确性验证,仅供参考。

Java字符编码问题

| Comments

1.假设文件用UTF-8保存了中文”操作计算机”,然后使用GBK编码进行读取?

1
2
3
4
String str = FileUtils.readFileToString(new File("/myfile"), "GBK");
System.out.println(str);
str = new String(str.getBytes("GBK"), "UTF-8");
System.out.println(str);

可以发现,后续转成UTF-8仍然有部分乱码,如果保存的内容是”操作计算”就不会乱码。为什么?

2.继续上述问题,如果使用ISO-8859-1进行读取?

1
2
3
4
String str = FileUtils.readFileToString(new File("/myfile"), "ISO-8859-1");
System.out.println(str);
str = new String(str.getBytes("ISO-8859-1"), "UTF-8");
System.out.println(str);

可以发现,可以发现无论是”操作计算机”还是”操作计算”、”操 作计算”,都不会乱码。为什么?

3.如果文件采用GBK编码保存中文,但是使用UTF-8读取,就会发现怎么转都是乱码? 为什么?

4.假设代码如下,为什么前面3行都是输出乱码?

1
2
3
4
5
System.out.println(new String("123你".getBytes("ISO-8859-1"), "ISO-8859-1"));
System.out.println(new String("123你".getBytes("ISO-8859-1"), "GBK"));
System.out.println(new String("123你".getBytes("ISO-8859-1"), "UTF-8"));
System.out.println(new String("123你".getBytes("GBK"), "GBK"));
System.out.println(new String("123你".getBytes("UTF-8"), "UTF-8"));

5.请思考,下面的同样掺和了ISO-8859-1,为什么却能正常?

1
2
System.out.println(new String(new String("123你".getBytes("GBK"), "ISO-8859-1")
        .getBytes("ISO-8859-1"), "GBK"));

6.假设使用http发送xml,那么xml报文采用何种编码发送和xml的编码头部指定的编码有什么关系?

1
<?xml version="1.0" encoding="GBK" ?>

使用net.sf.json库进行json反序列化时存在的问题

| Comments

问题描述

1
2
3
4
5
6
7
String content = "{\"response_head\":{\"menuid\":\"xxx\",\"process_code\":\"xxx\",\"verify_code\":\"\",\"resp_time\":\"20150107103234\",\"sequence\":{\"resp_seq\":\"20150107103301\",\"operation_seq\":\"\"},\"retinfo\":{\"retcode\":\"120\",\"rettype\":\"0\",\"retmsg\":\"[182096|]处理失败,原因:[屏蔽具体的失败原因!]\"}},\"response_body\":{} }";
JSONObject object = JSONObject.fromObject(content);
System.out.println(object.toString());

/*
{"response_head":{"menuid":"xxx","process_code":"xxx","verify_code":"","resp_time":"20150107103234","sequence":{"resp_seq":"20150107103301","operation_seq":""},"retinfo":{"retcode":"120","rettype":"0","retmsg":["182096|"]}},"response_body":{}}
*/

问题分析

采用json-lib-2.4-jdk15.jar,测试代码如上,会发现retmsg的值变成”[182096|”.

测试简化json字符串,最终效果如下:

解析失败的例子:

1
"{\"response_head\":{\"retmsg\":\"[182096|]处理失败,原因:[屏蔽具体的失败原因!]\"}}"

继续简化的话,就会解析成功

1
"{\"response_head\":\"[182096|]处理失败,原因:[屏蔽具体的失败原因!]\"}"

找了一下源码,发现json-lib在某些情况下(绕来绕去,断点发现的)会尝试解析字符串,看看是不是json对象。(尼玛,太智能了)

AbstractJSON.java中的260行,这个时候str是后面的内容。

1
2
3
4
5
6
7
     } else if( JSONUtils.mayBeJSON( str ) ) {
        try {
           return JSONSerializer.toJSON( str, jsonConfig );
        } catch( JSONException jsone ) {
           return str;
        }
     }

JsonArray.java中的1130行,这个时候v已经是”182096|”。这个时候会判断v是不是一个json对象,如果搞一个数组回去,否则就是搞一个字符串(上述现象)。

1
2
3
4
5
6
7
8
9
10
11
           tokener.back();
           Object v = tokener.nextValue( jsonConfig );
           if( !JSONUtils.isFunctionHeader( v ) ){
              if( v instanceof String && JSONUtils.mayBeJSON( (String) v ) ){
                 jsonArray.addValue( JSONUtils.DOUBLE_QUOTE + v + JSONUtils.DOUBLE_QUOTE,
                       jsonConfig );
              }else{
                 jsonArray.addValue( v, jsonConfig );
              }
              fireElementAddedEvent( index, jsonArray.get( index++ ), jsonConfig );
           }

例如,下面的情况会产生一个数组:

1
2
3
"{\"response_head\":{\"retmsg\":\"[{1820: 96|}]处理失败,原因:[屏蔽具体的失败原因!]\"}}"

{"response_head":{"retmsg":[{"1820":"96|"}]}}

关于如何判断是否是json,是会判断以[开头,以]结束的,刚好中枪。而尝试去截取中间内容的时候,又碰巧遇到中间的]字符,所以生成的字符串就是被截断了一部分的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
   /**
    * Tests if the String possibly represents a valid JSON String.<br>
    * Valid JSON strings are:
    * <ul>
    * <li>"null"</li>
    * <li>starts with "[" and ends with "]"</li>
    * <li>starts with "{" and ends with "}"</li>
    * </ul>
    */
   public static boolean mayBeJSON( String string ) {
      return string != null
            && ("null".equals( string )
                  || (string.startsWith( "[" ) && string.endsWith( "]" )) || (string.startsWith( "{" ) && string.endsWith( "}" )));
   }

问题结论

  • 当json对象中某个值是以”{“开头,”}”结束,或者”[“开头,”]”结束的时候,解析结果可能不是期望的。
  • 不幸的是,目前来看,这个问题是无解的,考虑使用其他json库吧。

关于编码与乱码问题

| Comments

关于java的编码

  • java的源代码编码格式和最终的运行是没什么关系的。你可以使用GBK或UTF-8来编程。
  • java编译后的class文件都是使用UTF-16来存储和运行的。
  • 在eclipse中是根据文件设置字符编码来编译的,所以可以对不同文件使用有不同的编码,但这个不推荐。
  • 使用javac编译可以通过-encoding指定字符编码,如果不指定,会使用系统默认编码,这个跟平台有关。所以使用ant需要指定编码。

下面这种在ant中常见的警告,就是表示编译用的编码和编程的编码不一致。

1
CCustGroupPrompt.java:43: 警告:编码 UTF-8 的不可映射字符

更多java相关的,见java字符编码问题

关于jsp的编码

  • jsp内容字符编码是pageEncoding指定的,用于指导jsp的编译器进行编译成java/class文件。如果没设置会采用contentType。
  • contentType是用于response的输出http报文时的编码,浏览器根据ContentType来采用何种字符编码显示。和使用response.setCharacterEncoding()是一个道理的。
1
<%@ page contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>

如果page设置的内容,和页面中的meta设置不一样,那又怎样?

  • meta是用来设置当前网页后续处理的默认编码,和当前页面的响应无关。
  • 只要page设置和jsp文件内容实际字符编码一致,就不会乱。

如果不设置page,那又怎样?

  • 如果没有指定page的话, contentType默认是text/html,浏览器会根据meta指定的编码来解析报文。这个时候,如果jsp内容实际编码和meta指定的编码一样,就能够显示正常。
  • 编译后的java文件内容,其实都是不能显示中文的。经测试,发现是用的ISO-8859-1读取的文件,并转换成UTF-8的java文件。
  • 如果使用UTF-16来编写jsp,但是不指定page,编译后的java和class反编译都是能够显示中文的。并且在tomcat下(其他未测试),即使meta设置的编码不一样,也能够显示中文,因为这个时候contentType变成text/html;charset=UTF-16BE,具体大家可以查看编译后的java文件。感觉在编译的时候能够优先识别到UTF-16一样。

上述情况只是在tomcat上测试过,并不代表其他在中间件也是同样的情况,实际应用中应该确保jsp内容的字符编码、page设置、meta设置保持一致,避免一些灵异事件。

关于URL的编码

这里指直接通过URL传递中文,或者手工拼接中文到URL的情况,究竟使用何种编码传递没有规定,看浏览器心情,不具可移植性。 对于IE来说,虽然高级选项上有个发送UTF-8 URL,但不一定会勾上。如果真的要使用,应该自行编码后传递。

普通表单提交

使用GET或者POST,对于编码来说,没有区别。 都会对中文进行编码,编码采用页面的字符编码。

例如”中文”的UTF-8编码是E4B8ADE69687,传递的内容就是%E4%B8%AD%E6%96%87。

页面编码是通过指定的。 而html5页面可以通过这种简化形式指定。

表单文件上传

需要使用POST,并在form中增加属性enctype=”multipart/form-data”。 不会对文件名、输入框内容进行字符编码。 采用页面的字符编码,对内容原样传输。

区别可能不是很好理解,下面举例: 例如”中文”的UTF-8编码是E4B8ADE69687,传递的内容是字节E4B8ADE69687,或许在某些工具上可以直接看到“中文”.(像fiddler用utf-8来显示的)

Java/Servlet/Struts2(commons-upload)对参数的处理

Java/Servlet对参数的处理 * 默认只能获取到普通表单的参数提交。 * 编码格式通过request.setCharacterEncoding(“UTF-8”)指定,这个已经有过滤器可以实现的了。 * 使用Struts2的话,对multipart/form-data的提交也是能够获取通过getParameter取到参数的。

注意的是,有些实现(如tomcat),对参数的解析是延后处理的,设置了编码之后,获取一个参数(这个时候参数全部都解析了),再设置编码是没有效果。ServletRequest的setCharacterEncoding描述也是这么说的。

标准的commons-upload,文件名的获取、输入框内容的获取使用的编码可能不一样。 * 文件名的获取,就是FileItem.getName(),解析编码需要通过ServletFileUpload#setHeaderEncoding这个方法设置,如果没有设置,采用平台编码(可以通过-Dfile.encoding=UTF-8来指定,否则win通常是ANSI(GBK),unix看locale) * 输入框内容,就是FileItem.getString(),可以指定解析编码,如果不指定采用ISO-8859-1。

Struts2默认使用commons-upload进行文件上传的处理。 * 对于文件名的获取没有通过setHeaderEncoding设置,所以这个通常会依赖于平台编码(需要确保平台编码和页面编码一致) * 对于输入框内容的获取,指定了编码格式为request.getCharacterEncoding(),否则采用默认的ISO-8859-1。所以这个需要提前设置一下CharacterEncoding,否则也可能会乱码。

常见DES实现陷阱

| Comments

DES要点说明

  • DES走的是分组加密,每次处理对象的是8位byte,所以对字符串加解密的时候,会涉及字符编码格式和补齐8位的问题。
  • DES的密钥是固定8位的byte的,其中前7位是加解密用的,最后一位是校验码。
  • 3DES的增强型的DES,带3个key,如果3个key一样,就是DES,也有一种变种是1、3是一样的。但都是固定8位的。
  • 3DES通常是EDE,就是先加密(k1)再解密(k2)再加密(k3)

目前,项目代码中有3个和DES实现相关的类,下面看看他们有哪些问题:

案例1

  • 从字符串到byte的转换,有指定编码格式GBK,这个是可以接受的。
  • 使用的是DESede,就是3DES的EDE加密方式,但是3个key是一样的,没有意义。
  • 加密时代码先自行进行了补齐操作(补\0),但是补齐是在字符串上操作的,不是在字节上操作,导致实际上可能没有对齐(中文情况)。
  • 调用加密API时,没有指定补齐方式,会采用默认补齐,造成重复补齐(当然也修复了上面的补齐操作)。
  • 解密指定NoPadding,和加密Padding方式不一样,造成解密结果最后会出现很多多余的字节。所以结果必须得trim一下才行。

参考代码如下:

补齐实现有误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public String encrypt(String in) throws Exception {
    String strIn = in;
    if (null == strIn || "".equals(strIn)) {
        return "";
    }

    int i = 0;
    i = strIn.length() % 8;

    if (0 == i) {
        for (i = 0; i < 8; i++) {
            strIn += "\0";
        }
    } else {
        while (i > 0) {
            strIn += "\0";
            i--;
        }
    }
    byte[] bytes = strIn.getBytes(CHARSET);
    byte[] enbytes = encryptCipher.doFinal(bytes);
    return byteArrToHexStr(enbytes);
}

key是一样的,补齐方式没对应上:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public DESedeEncrypt() {
    byte[] buffer = new byte[] {
            0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31,
            0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31, 0x31
    };

    SecretKeySpec key = new SecretKeySpec(buffer, "DESede");

    try {
        encryptCipher = Cipher.getInstance(KEY_ALGORITHM);
        encryptCipher.init(Cipher.ENCRYPT_MODE, key);
        decryptCipher = Cipher.getInstance("DESede/ECB/NoPadding");
        decryptCipher.init(Cipher.DECRYPT_MODE, key);
    } catch (NoSuchAlgorithmException e) {
        Throwables.propagate(e);
    } catch (NoSuchPaddingException e) {
        Throwables.propagate(e);
    } catch (InvalidKeyException e) {
        Throwables.propagate(e);
    }
}

案例2

  • 从字符串到byte的转换,采用了系统默认编码,存在平台移植性问题。
  • 密钥key的长度布置8位,有多余字符(虽然只取前8位避免出错),造成混乱。

key的格式不标准,有多余字符:

1
2
3
4
5
6
7
8
9
10
11
private static String strDefaultKey = "mywebsite123456%";
private Key getKey(byte[] arrBTmp) throws Exception {
    byte[] arrB = new byte[8];
    for (int i = 0; i < arrBTmp.length && i < arrB.length; i++) {
        arrB[i] = arrBTmp[i];
    }

    Key key = new javax.crypto.spec.SecretKeySpec(arrB, "DES");

    return key;
}

案例3

  • 从字符串到byte的转换,采用了系统默认编码,存在平台移植性问题。
  • 实现不是标准的DES,或3DES,是在DES基础上定义了一套加密。
  • 根据目前key的长度,比标准3DES都要慢很多,另外没有采用JDK带的API。

key的长度不标准:

1
2
3
4
5
public class DesUtil {
    public static final String firstKey = "com.xxx.xxxpro";
    public static final String secondKey = "xxx_web";
    public static final String thirdKey = "xxxservice";
}

实现方式是对每个key补齐8位,再切割形成每组多个8位的key,再采用EEE的方式进行处理:

1
2
3
4
5
6
7
8
9
                    for (x = 0; x < firstLength; x++) {
                        tempBt = enc(tempBt, (int[]) firstKeyBt.get(x));
                    }
                    for (y = 0; y < secondLength; y++) {
                        tempBt = enc(tempBt, (int[]) secondKeyBt.get(y));
                    }
                    for (z = 0; z < thirdLength; z++) {
                        tempBt = enc(tempBt, (int[]) thirdKeyBt.get(z));
                    }