自从进入android,接触java以来,已与jvm及其gc机制有过多次“奇妙”接触,jvm的gc实现有很多种,策略也不一样,据我自己亲身体会,即使是android手机的各个厂商的实现(当然,android上面的严格说来并不是jvm)甚至都不一样,让你遇到的话真是欲哭无泪。比如说涉及到softreference或者weakreference的时候,你发现程序的行为和文档说明不一致的时候,请千万不要惊奇。
话说android的文档很多地方也不完善,比如说Bitmap,文档上明明说一般不需要recycle,可是用过的同志应该明白,你敢不recycle吗?(咱不较真儿啊。。。)当然,话题扯远了,发个牢骚,这不,前两天又遇到了一次问题。这个问题之前遇到过一次,百思不得其解,稀里糊涂蒙混过关了。这次再一次遇见,经过仔细观察,认真思考,做实验,查数据,好好纠结,终于寻得要领,并且经过了验证。
假设有一个class名为PublicArea,另外有一个Class名为ZipUtil,前者主要用于数据交换,里面有一些全局数据,后者提供了一个zip相关的static方法,用于从byte[]或者Inputstrem对象中提取需要的指定文件名的文件数据.
PublicArea类定义了两个全局变量如下:
public static byte[] mZipdata=null;
public static InputStream mZipIS=null;
ZipUtil类定义如下:
public class ZipUtil{
public static Map<String,byte[]> unzipFilesFromStream(InputStream is,String type) throws Exception
{
Map<String, byte[]> result = new HashMap<String, byte[]>();
ZipInputStream zipStream=new ZipInputStream(is);
ZipEntry zipEntry;
while((zipEntry=zipStream.getNextEntry())!=null){
if (zipEntry.getName().endsWith("."+type))
{
int zipsize=(int)zipEntry.getSize();
byte[] temp=new byte[zipsize];
byte[] beRead=new byte[zipsize];
int nowpos=0;
int beReadThisTime=0;
while(-1!=(beReadThisTime=zipStream.read(temp)))
{
System.arraycopy(temp,0,beRead,nowpos,beReadThisTime);
nowpos+=beReadThisTime;
temp=new byte[zipsize];
}
result.put(zipEntry.getName(), beRead);
}
zipStream.closeEntry();
}
zipStream.close();
return result;
}
public static byte[] unzipFileFromStream(InputStream is,String filename) throws Exception
{
byte[] result =null;
ZipInputStream zipStream=new ZipInputStream(is);
ZipEntry zipEntry;
while((zipEntry=zipStream.getNextEntry())!=null){
if (zipEntry.getName().equals(filename))
{
int zipsize=(int)zipEntry.getSize();
byte[] temp=new byte[zipsize];
byte[] beRead=new byte[zipsize];
int nowpos=0;
int beReadThisTime=0;
while(-1!=(beReadThisTime=zipStream.read(temp)))
{
System.arraycopy(temp,0,beRead,nowpos,beReadThisTime);
nowpos+=beReadThisTime;
temp=new byte[zipsize];
}
result=beRead;
}
zipStream.closeEntry();
}
zipStream.close();
return result;
}
}
我在PublicArea中通过一系列动作获取了一份zip文件的二进制数据(比如file读取或者从网络传输)并将对象引用交给mZipdata,然后使用如下代码:
mZipIS = new ByteArrayInputStream(mZipdata);
创建字节流并将引用赋值给mZipIS 。
至此,mZipdata与mZipIS已创建相应对象,同时因为二者均为全局变量,所以除非我手动放弃引用,否则这两个对象将一直存活。
ZipUtil中的两个方法分别为:
public static byte[] unzipFileFromStream(InputStream is,String filename)
public static Map<String,byte[]> unzipFilesFromStream(InputStream is,String type)
前者根据给定的InputStream获取zip包中名为filename的文件的二进制字对象。
后者是遍历zip包中的文件并将后缀名为type的文件的二进制字节对象存入一个HashMap中,key为文件名。
在这里说一下,zip用来做遍历速度很快,它的结构适合提取文件名。
问题来了,我需要分别使用ZipUtil中的这两个方法,先使用unzipFileFromStream,再使用unzipFilesFromStream,InputStream 参数当然就是PublicArea中的mZipIS了。但是在第二次调用ZipUtil的方法的时候,会爆出“NullPointerException”,着实蛋疼。mZipIS的对象是在第一次调用unzipFileFromStream的时候创建的,然后方法跑完,其他线程过一会儿会来调用unzipFilesFromStream方法,仍然想当然地使用这个mZipIS作为参数,然后就可耻地异常。ZipUtil被回收了。其实在开始的时候我还刻意没有去在ZipUtil的实现中调用zipStream.close(); 这样表面上看应该不会存在这样的问题啊,那么这个问题是怎样产生的呢?这就要从多个方面说起了。
首先我们要复习一下java的io类的一个小特点,java的io类实现堪称经典,所有的流式类的实现分为节点流和过滤流两种,用了一种“装饰设计模式”,一环套一环,形成一种链式结构。这里要说一下它的资源释放,当你将各种过滤流串起来之后,关闭的时候,你只需要对最后一个过滤流(就是串的尾端末梢)的引用调用close()方法即可释放整个链式结构里的流对象。
而我遇到的问题是因为在ZipUtil的方法实现中,在mZipIS尾端又加入了一个ZipInputStream过滤流,而它是局部变量,所以。。。方法执行完它就被华丽丽地释放了,不光它被释放,整个链式结构都被释放,连累到了mZipIS。虽然mZipIS的引用仍然存在,但是dv虚拟机的gc依然将它回收了,一方面dv虚拟机的gc策略是尽量回收,另一方面我们也要认识到,gc的智能毕竟是有限的,同时程序员良好的编码习惯很重要,肯定不存在完美的jvm和gc,不要将自动回收的机制掺和到太过于复杂的程序流程,否则真的是自讨苦吃,对于简单的程序流程来说,大部分的jvm的gc行为应该基本一致,因为没什么好选择的,所以相对安全。虽然我们可以把很多问题怪罪于jvm和他gc不够完善,但是。。。你能和机器较什么劲儿呢 - -#
这里我们也看到,对于mZipdata这种全局数据存储的引用的使用,在哪里产生流就在哪里结束,不要依赖于一个流对象幻想用个千八百遍,你组合的一连串的面向需求的链式结构用完了就整体销毁掉,养成良好的习惯,你自己手工销毁那你自然该知道在用的时候创建。至于对数据引用的保持,还是使用byte[]引用吧,用的时候创建流,这样条理清晰,也不麻烦,不能太懒啊。当然了,极端情况你看着办吧,比如神马网络不顺畅并且没有存储空间并且内存不够。。。。那就让程序自杀好了(开玩笑的),跑题了,哈哈。
对了,那个zip工具类可以直接拿去用哦,有问题欢迎探讨指教。