分类目录归档:Java

Android 中解决图像解码导致的 OOM 问题

在上一篇博文 Android Bitmap 内存限制 中我们详细的了解并分析了 Android 为什么会在 Decode Bitmap 的时候出现 OOM 错误,简单的讲就是 Android 在解码图片的时候使用了本地代码来完成解码的操作,但是使用的内存是堆里面的内存,而堆内存的大小是收 VM 实例可用内存大小的限制的,所以当应用程序可用内存已经无法再满足解码的需要时,Android 将抛出 OOM 错误。

这里讲一个题外话,也就是为何 Android 要限制每个应用程序的可用内存大小呢?其实这个问题可能有多方面的解答,目前我自己考虑到的有两点:

  1. 使得内存的使用更为合理,限制每个应用的可用内存上限,可以防止某些应用程序恶意或者无意使用过多的内存,而导致其他应用无法正常运行,我们众所周知的 Android 是有多进程的,如果一个进程 (也就是一个应用) 耗费过多的内存,其他的应用还搞毛呢?当然在这里其实是有一个例外,那就是如果你的应用使用了很多本地代码,在本地代码中创建对象解码图像是不会被计算到的,这是因为你使用本地方法创建的对象或者解码的图像使用的是本地堆的内存,跟系统是平级的,而我们通过 Framework 调用 BitmapFactory.decodeFile() 方法解码时,系统虽然也是调用本地代码来进行解码的,但是 Android Framework 在实现的时候,刻意地将这部分解码使用的内存从堆里面分配了而不是从本地堆里分配的内存,所以才会出现 OOM,当然并不是说从本地堆里分配就不会出现 OOM,本地堆分配内存超过系统可用内存限制的话,通常都是直接崩溃,什么错误可能都看不到,也许会有一些崩溃的错误字节码之类的。
  2. 省电的考虑,呃…,原因我好像也不能很明白地说出来。

回到正题来,我们在应用的设计和开发中可能会经常碰到需要在一个界面上显示数十张图片乃至上百张,当然限于手机屏幕的大小我们通常在设计中会使用类似于列表或者网格的控件来展示,也就是说通常一次需要显示出来图片数还是一个相对确定的数字,通常也不会太大。如果数目比较大的画,通常显示的控件自身尺寸就会比较小,这个时候可以采用缩略图策略。下面我们来看看如果避免出现 OOM 的错误,这个解决方案参考了 Android 示范程序 XML Adapters 中的 ImageDownloader.java 中的实现,主要是使用了一个二级缓存类似的机制, 就是有一个数据结构中直接持有解码成功的 Bitmap 对象引用,同时使用一个二级缓存数据结构持有解码成功的 Bitmap 对象的 SoftReference 对象,由于 SoftReference 对象的特殊性,系统会在需要内存的时候首先将 SoftReference 对象持有的对象释放掉,也就是说当 VM 发现可用内存比较少了需要触发 GC 的时候,就会优先将二级缓存中的 Bitmap 回收,而保有一级缓存中的 Bitmap 对象用于显示。

其实这个解决方案最为关键的一点是使用了一个比较合适的数据结构,那就是 LinkedHashMap 类型来进行一级缓存 Bitmap 的容器,由于 LinkedHashMap 的特殊性,我们可以控制其内部存储对象的个数并且将不再使用的对象从容器中移除,这就给二级缓存提供了可能性,我们可以在一级缓存中一直保存最近被访问到的 Bitmap 对象,而已经被访问过的图片在 LinkedHashMap 的容量超过我们预设值时将会把容器中存在时间最长的对象移除,这个时候我们可以将被移除出 LinkedHashMap 中的对象存放至二级缓存容器中,而二级缓存中对象的管理就交给系统来做了,当系统需要 GC 时就会首先回收二级缓存容器中的 Bitmap 对象了。在获取对象的时候先从一级缓存容器中查找,如果有对应对象并可用直接返回,如果没有的话从二级缓存中查找对应的 SoftReference 对象,判断 SoftReference 对象持有的 Bitmap 是否可用,可用直接返回,否则返回空。

主要的代码段如下:

private static final int HARD_CACHE_CAPACITY = 16;

// Hard cache, with a fixed maximum capacity and a life duration
private static final HashMap<String, Bitmap> sHardBitmapCache = new LinkedHashMap<String, Bitmap>(HARD_CACHE_CAPACITY / 2, 0.75f, true) {
    private static final long serialVersionUID = -57738079457331894L;

    @Override
    protected boolean removeEldestEntry(LinkedHashMap.Entry<String, Bitmap> eldest) {
        if (size() > HARD_CACHE_CAPACITY) {
            // Entries push-out of hard reference cache are transferred to soft reference cache
            sSoftBitmapCache.put(eldest.getKey(), new SoftReference<Bitmap>(eldest.getValue()));
            return true;
        } else
            return false;
    }
};

// Soft cache for bitmap kicked out of hard cache
private final static ConcurrentHashMap<String, SoftReference<Bitmap>> sSoftBitmapCache = new ConcurrentHashMap<String, SoftReference<Bitmap>>(HARD_CACHE_CAPACITY / 2);

/**
* @param id
*            The ID of the image that will be retrieved from the cache.
* @return The cached bitmap or null if it was not found.
*/
public Bitmap getBitmap(String id) {
    // First try the hard reference cache
    synchronized (sHardBitmapCache) {
        final Bitmap bitmap = sHardBitmapCache.get(id);
        if (bitmap != null) {
            // Bitmap found in hard cache
            // Move element to first position, so that it is removed last
            sHardBitmapCache.remove(id);
            sHardBitmapCache.put(id, bitmap);
            return bitmap;
        }
    }

    // Then try the soft reference cache
    SoftReference<Bitmap> bitmapReference = sSoftBitmapCache.get(id);
    if (bitmapReference != null) {
        final Bitmap bitmap = bitmapReference.get();
        if (bitmap != null) {
            // Bitmap found in soft cache
            return bitmap;
        } else {
            // Soft reference has been Garbage Collected
            sSoftBitmapCache.remove(id);
        }
    }

    return null;
}

public void putBitmap(String id, Bitmap bitmap) {
    synchronized (sHardBitmapCache) {
        if (sHardBitmapCache != null) {
            sHardBitmapCache.put(id, bitmap);
        }
    }
}

上面这段代码中使用了 id 来标识一个 Bitmap 对象,这个可能大家在实际的应用中可以选择不同的方式来索引 Bitmap 对象,图像的解码在这里就不做赘述了。这里主要讨论的就是如何管理 Bitmap 对象,使得在实际应用中不要轻易出现 OOM 错误,其实在这个解决方案中,HARD_CACHE_CAPACITY 的值就是一个经验值,而且这个跟每个应用中需要解码的图片的实际大小直接相关,如果图片偏大的话可能这个值还得调小,如果图片本身比较小的话可以适当的调大一些。本解决方案主要讨论的是一种双缓存结合使用 SoftReference 的机制,通过使用二级缓存和系统对 SoftReference 对象的回收特性,让系统自动回收不再敏感的图片 Bitmap 对象,而保有一级缓存也就是敏感的图片 Bitmap 对象。

Android 上如何正确实现程序的联网,事关 WIFI/CMWAP/CMNET

我想很多 Android 程序开发者都曾碰到过这样的问题,那就是如何让自己的程序在国内如此复杂的网络环境下顺利的接上网络,给我们的用户一个更好的体验。
从网络上一些已有的数据来看,Android 手机用户群体的联网率普遍比较高,联网的方式非常多样,最多的还是使用 WIFI,当然 WIFI 速度和资费上的优势让她成为了每一个玩机者的首选网络接入方式,但是很多的时候我们的条件并不是那么的尽如人意。例如在公车或地铁上,我们这些诸多支付不起 3G 资费的人士,首选的接入方式依然是 CMWAP/CMNET, 而由于国内网络的一些个问题,选择这两个或者其他的 APN 会有一些问题,问题就是我们可能需要设置代理才可以顺利登录网络。

以下是我自己在网络上寻找解决方案的时候,收集的一些信息,记录如下:

WAP 是一中手机上网的协议。CTWAP 是中国电信的 WAP 接入名称(China Telecom WAP),CMWAP 是中国移动的 WAP 接入名称(China Mobile WAP),  UNIWAP 是联通的 WAP 接入名称(china Unicom WAP),  另外 CTNET/CMNET/UNINET 同上。
CTWAP 的上网网关:10.0.0.200:80
CMWAP 的上网网关:10.0.0.172:80
UNIWAP 使用的网关与 CMWAP 一致
我们可以通过 MCC+MNC 码的方式来进行简单的判断,但是实际上这种方式并不是完全正确的方法(自己在项目上碰到了该问题, 因为实际情况中我们总是需要面对多种网络的情况)。这个时候其实我们可以稍微 Hack 一下,虽然 Android 并没有提供非常好的 API,不过我们可以通过一些方法绕过去这里有一篇非常不错的文章 http://www.javaeye.com/topic/565662 ,讲解得还算全面。
下面给出我自己的解决方案:

该方案在打开 WIFI/CMWAP/CMNET 的情况下均单独测试成功。
同理 HttpPost 也可以如法炮制,下面附上一段代码:

LineNumberReader 和 FileWriter 同时使用碰到的问题

今天上班有个任务就是将之前产品中的示范代码有一些不规范的地方进行修改,使得能在多平台顺利运行,主要是文件名大小写的问题。由于之前示范代码编写者在跨平台上编码经验相对不足,所以编写者在写代码的时候并没有严格的大小写意识 (主要原因还是因为 Windows 不区分大小写,而代码的测试均是在 Windows 下进行的),造成很多遗留问题。产品中有示范代码 64 个,大小写问题几乎无处不在,还有一个就是跨平台产品支持的数据引擎与 Windows 不一致,之前的 Windows 版本中有不少的示范代码使用了 Windows 平台特定的引擎类型,也需要进行修改。

修改的方法可以有很多,最为简单和直接的方法,应该是一个个程序运行,一个个文件检查,逐个进行修改。我想作为程序员的我们,肯定是不愿意做这么无聊而重复的工作的。那么搞定这个无聊而重复的工作呢?首先分析一下已有的示范代码的特征,虽然之前的代码存在诸多不规范的地方,但是在一些方面还是存在很多共性的,大小写出现错误的肯定是在设置数据路径的时候出现的,设置引擎类型的代码更是固定的,之需要使用几个非常简单的正则表达式就可以将其完全正确替换了。所以我们选择了读写文件的方法来完成这个任务。

在实现这个功能的时候,使用到了 LineNumberReader 和 FileWriter 两个类,每读取一行,使用 replaceAll(String regx,String str) 方法对该行进行替换,之后再使用 FileWriter 将替换后的文本写入文件:

执行的结果是,64 个示范代码中,总会有几个会出现一些很奇怪的问题,那就是修改后的文件丢失了很多文本,导致编译不通过。那么问题在哪儿呢?经过多次分析和调试,最终发现当 LineNumberReader 和 FileWriter 指向同一个文件的时候会出现该问题,如果将源文件处理后的结果写入到一个新的文件中便不会存在这样的问题。那么究竟是哪里出了问题呢?来看一张图

在这张图中我们能看到一些信息,LineNumberReader 对象中有一个 cb 字符数组,大小为 8192=1024*8.

在我整个的调试过程中,我一直关注着最后一个字符,在整个过程中,这个位置的字符一直没有发生改变,而最终写入到文件中的最后一个字符就是该字符,这难道只是一个巧合吗?

现在让我们更换一下代码将原本使用同一个 File 对象的代码,改成使用另一个新文件对象的代码来重新调试一下。

先执行代码,查看结果发现,文本的内容完全被写入到新文件中,并没有出现丢失文本这样的情况,那么我们再来调试一下 ,我们再次来监视 cb[8191] 处的值,这次我们可以发现在 LineNumberReader 对象 lineNumber==319 的时候,该值发生改变。

====我是分割线======

综合上面的两种情况,我们可以得出一个结论,当 LineNumberReader 和 FileWriter 同时指向一个文件的时候,系统在写入文件的时候并不是逐行逐行写入,而是先记录下来逐行的内容,并且是记录到 LineNumberReader 的 cb 字符数组中。该数组的大小固定为内存最小单位 8K,也就是大小为 8192 的数组,该数组将保存初次初始化 FileWriter 对象时读取的文件内容,在之后的每次 FileWriter 写入操作均是改变该数组中存储的字符值。这样导致的结果就是,如果当前的读取的文件较大,就会存在丢失内容的问题 (最多保存 8192 个字符, 含空格和换行)。而使用一个新的文件来进行修改后内容的写入就不存在这样的问题,当 FileWriter 需要使用到 flush() 方法来将当前缓冲区中的所有内容写入文件之后,LineNumberReader 中的 cb 数组内容也将发生改变,指针下移,读取下一个块大小为 8192 的内容,直到文件末尾。

所以在使用 LineNumberReader 和 FileWriter 的时候需要注意这一点,如果实在不愿意多生成一堆新文件的话,可以在写入完成之后,将原始文件删除,而将新文件更名一下就好了。

关于 JTree 的一些碎碎念

本博号称关注 Web2.0, Ruby/Rails,Java。但是据本人所知,到目前为止还没有任何一篇关于 Java 方面的文章和只言片语。作为本博的博主,确实有点大言不惭的感觉,一想到这个心中就颇不宁静啊。

其实作为一名博客,写东西的欲望一定要强烈,很多的时候我们并没有很多的素材可以写,因为生活几乎每天都是 Just so so,我们谁都不想书写平淡,总想语不惊人死不休。作为博客的人们可能这种感觉更为强烈,我是一名独立博客耶,我不能人云亦云啊,我得从我的文章里抒发我的思想啊。艹,你娘的思想也就大街上小摊上的胡萝卜包子一般廉价,还装。所以呢,我们还是需要不时的刺激一下自己的神经,同时呢,也给我们伟大的祖国发育不良的国联网添加一些有趣或无趣的素材资料吧,也算是为我国早日实现四个现代化添砖加瓦了。

===========我是分割线=========

树中特定的节点可以由 TreePath(封装节点及其所有祖先的对象)标识,或由其显示行(其中显示区域中的每一行都显示一个节点)标识。展开 节点是一个非叶节点(由返回 false 的 TreeModel.isLeaf(node) 标识),当 展开 其所有祖先时,该节点将显示其子节点。 折叠 节点是隐藏它们的节点。 隐藏 节点是位于折叠祖先下面的节点。所有 可查看 节点的父节点都是可以展开的,但是可以显示它们,也可以不显示它们。 显示 节点是可查看的并且位于可以看到它的显示区域。

这是 JDK 5.0 非官方中文文档中的描述,我想这个描述给我们的第一个直观印象就是这个控件应该和 Windows 资源管理器中的树状结构的表现是一致的。确实如此,JTree 确实是用来作为树形展示使用的,因为我们在很多的数据管理和业务处理上,秉承了我们自古就袭承的分类方式,所以我们有很多的业务可能用到或者说可以用到 JTree。

那么 JTree 怎么用的呢?这个问题我曾经问过自己很多次,还做过很多相关的工作来学习 JTree 的使用,记得来到北京实习做的第一个学习阶段作业就是使用公司已有的组件产品做一个叠加分析的 Demo。作为一个比较友好的叠加分析工具,就肯定需要一些 GIS 数据处理的功能,比如打开工作空间,数据源,数据集,地图,同时还应该能简单地做一些图层删除或者地图关闭之类的基础功能。这样一来,主体的工作就是基础功能模块的实现了,只是在基础功能模块的基础上,添加一个叠加分析功能模块,而在基础功能模块中的可视化显示 JTree 必然是首选控件。

在实现树状加载和显示工作空间,参考公司已有的桌面产品 UI 设计,需要再 JTree 的每个节点之前添加一些图标,用于标识当前节点的数据类型,例如线数据集和面数据集的图标是有明显差异的,这样对用户的友好度会提高很多。默认的 JTree 使用 Java 默认的 meta 风格显示文件夹 (根节点) 和文件 (叶节点) 的图标样式,那么如何添加自定义的图标显示呢?当时我就在 JTree 的文档中苦苦找寻,希望自己能找到一个 setIcon() 的方法,最终无耻的失败鸟。后来才知道之需要自己实现一个类,实现 TreeCellRenderer 就可以了,重写下面这个方法 (Sun 就是这么排版的,我觉得这样挺好的,不要怪我占太多行了)

Component getTreeCellRendererComponent(JTree tree,

在这个方法中,它的返回值为一个 Component,那么我们完全可以自由发挥了,比如返回一个 JLabel。当时欣喜若狂,赶紧将代码巴拉巴拉地敲好,一看效果显著啊,根据类型不同生成不同的 Icon 设置给将要返回的 JLabel 对象就 OK 了。

实习的工作相对简单,当时对 JTree 的理解也就点到为止了。那么此次项目组需要退出 Objects Java 的控件,其中就有一些控件需要使用到 JTree 来实现,功能要求相对就复杂了不少。

需要在一个节点文本前,显示多个图标,除了用于区别节点数据类型的图标外,还需要添加几个操作图标,用于可视化节点数据的操作,那么返回一个什么呢?JPanel 无疑是最好的选择,轻量化容器中的万金油。在 JPanel 中添加几个可视化的 Icon 并不是难事,只是 JLabel 多少的问题了,通过对节点数据类型的判断分别设置便好了。问题是,如何判断用户当前点击的位置落在 JPanel 中的那个操作图标上呢?又如何将这些操作直接反映到实际的数据上来呢?通过 JTree.getUI() 方法获取当前的 JTree 的 UI,然后调用 TreeUI.getPathBounds(JTree tree, TreePath path) 方法获取当前节点的绘制区域,之后通过鼠标事件 MouseEvent 的 getX() 和 getY() 方法获取当前鼠标的位置信息,由于控件的编写图标的大小由自己定义,所以可以确定图标的大小和 JPanel 的布局 (我使用的是 FlowLayout,并且使用 setHgap() 方法将水平控件间距设置为 0),通过像素坐标的计算便可以得出当前用户鼠标单击事件应该响应哪个对应索引处的动作。

在实现的过程中,还需要使用到编辑节点的功能,那么如何来做呢?首选实现 TreeCellEditor 接口,完全自己定制,可以避免像继承自 DefaultTreeCellEditor 的烦恼 (因为它能满足一般需求,但是会限制你拳脚)。TreeCellEditor 也有一个很嚣张的方法,号称自己要接管 TreeCellRenderer 的方法,就是这个啦

Component getTreeCellEditorComponent(JTree tree, Object value, boolean isSelected, boolean expanded, boolean leaf, int row)

重写这个方法,用来自定义编辑时的节点渲染,例如我的树形控件在编辑的时候只能编辑节点名称,那么前面的图标还是要保持与未编辑时一致的,编辑时只需要将后面的文本置换为一个 JTextField 就可以了。然后为该 JTextField 注册一个键盘事件监听器,当输入 Enter 的时候,调用 TreeCellEditor 的 stopEditing() 方法,触发停止编辑事件。如果我们需要将编辑保存至数据模型中的话,就在 stopEditing() 方法中编写业务代码即可。

综上 JTree 这个控件实际上是一个完全遵循 MVC 架构的设计,甚至比 MVC 还抽象一些。JTree 中只处理数据和显示的一些控制,例如判断当前节点是否能够被编辑的方法 isPathEditable(TreePath path) 等。数据模型完全可以委托为 TreeModel 来管理,之需要使用 setModel 便可以轻松地将 JTree 和 TreeModel 关联起来。而在其显示渲染上便是完全由 TreeCellRenderer 来接管的,只需实现一个类,该类实现 TreeCellRenderer 接口,重写 getTreeCellRendererComponent(…) 方法,便可实现完全自定义面板显示节点。抽象最高层的是,当节点为可编辑状态时,其渲染又交由 TreeCellEditor 接管了,在编辑节点时,TreeCellEditor 的 getTreeCellRendererComponent(…) 方法便将完全接管节点的渲染工作,并且控制编辑时的动作,例如取消编辑和停止编辑之类的种种。这么看来,其实 JTree 只是提供一个壳子,通过其内部的 TreeMod 存储数据,外部的 TreeCellRenderer 和 TreeCellEditor 来控制其显示渲染和编辑渲染,给开发人员提供了一个可高度定制化的控件接口。

==========我还是分割线=========

本文的形成是如行云流水账一般啊,我也有点不知所云了,众位看官,能看则看,不能看则拉倒吧。JDK API 文档在你家里叫你回家翻翻呢。