HttpURLConnection 实现网络访问文件,并且将获取到的数据存放到字节数组中
1 public class HttpURLConnHelper { 2 private final static String TAG = "MyHttpHelperUtil"; 3 4 /** 5 * 作用:实现网络访问文件,将获取到的数据存在字节数组中 6 * 7 * @param url 8 * :访问网络的url地址 9 * @return byte[] 10 */ 11 public static byte[] downLoadByteFromURL(String url) { 12 Log.i(TAG, "==以get方式访问网络"); 13 HttpURLConnection httpConn = null; 14 //BufferedInputStream 是一个带有缓冲区域的inputStream 15 BufferedInputStream bis = null; 16 ByteArrayOutputStream baos = null; 17 try { 18 Log.i(TAG, "==1、创建URL对象,url-->" + url); 19 URL urlObj = new URL(url); 20 Log.i(TAG, "==2、创建连接,openConnection"); 21 httpConn = (HttpURLConnection) urlObj.openConnection(); 22 // 以下两项都是默认值。虽然可以不写,但是建议写上。 23 httpConn.setRequestMethod("GET"); 24 // // 设置将服务器返回数据写入到httpConn对象 25 // httpConn.setDoInput(true); 26 // 设置访问超时时间 27 httpConn.setConnectTimeout(1000); 28 // // 设置是否使用缓存 29 // httpConn.setUseCaches(false); 30 // // 建立远程对象实际连接 31 // httpConn.connect(); 32 Log.i(TAG, 33 "==3、getResponseCode()方法获取服务器的返回码:" 34 + httpConn.getResponseCode()); 35 if (httpConn.getResponseCode() == 200) { 36 Log.i(TAG, 37 "==4、调用 HttpURLConnection对象的getInputStream()方法获取服务器返回的流信息"); 38 bis = new BufferedInputStream(httpConn.getInputStream()); 39 Log.i(TAG, "==5、执行标准的IO流操作"); 40 baos = new ByteArrayOutputStream(); 41 int c = 0; 42 byte[] buffer = new byte[8 * 1024]; 43 while ((c = bis.read(buffer)) != -1) { 44 baos.write(buffer, 0, c); 45 baos.flush(); 46 } 47 byte[] result = baos.toByteArray(); 48 Log.i(TAG, "result==" + result.length); 49 return result; 50 } 51 } catch (Exception e) { 52 e.printStackTrace(); 53 } finally { 54 try { 55 if (bis != null) { 56 bis.close(); 57 } 58 if (baos != null) { 59 baos.close(); 60 } 61 if (httpConn != null) { 62 httpConn.disconnect(); 63 } 64 } catch (IOException e) { 65 e.printStackTrace(); 66 } 67 } 68 return null; 69 }
BufferedInputStream是一个带有缓冲区域的InputStream,它的继承体系如下:
InputStream
|__FilterInputStream
|__BufferedInputStream
FilterInputStream:
FilterInputStream通过装饰器模式将InputStream封装至内部的一个成员变量:
- protected volatile InputStream in;
需要注意的是该成员变量使用了volatile关键字进行修饰,这意味着该成员变量的引用的内存可见性为多线程即时可见的。
其它地方FilterInputStream将所有的操作委托给了in这个成员进行操作。
了解了这些过后,来仔细看看BufferedInputStream的成员变量:
1 private static int defaultBufferSize = 8192 //该变量定义了默认的缓冲大小 2 3 protected volatile byte buf[]; //缓冲数组,注意该成员变量同样使用了volatile关键字进行修饰,作用为在多线程环境中,当对该变量引用进行修改时保证了内存的可见性。 4 5 private static final AtomicReferenceFieldUpdater<BufferedInputStream, byte[]> bufUpdater = AtomicReferenceFieldUpdater.newUpdater(BufferedInputStream.class, byte[].class, "buf")//缓存数组的原子更新器,该成员变量与buf数组的volatile关键字共同组成了buf数组的原子更新功能实现。 6 7 protected int count;//该成员变量表示目前缓冲区域中有多少有效的字节。 8 9 protected int pos;//该成员变量表示了当前缓冲区的读取位置。 10 11 protected int markpos = -1;/*表示标记位置,该标记位置的作用为:实现流的标记特性,即流的某个位置可以被设置为标记,允许通过设置reset(),将流的读取位置进行重置到该标记位置,但是InputStream注释上明确表示,该流不会无限的保证标记长度可以无限延长,即markpos=15,pos=139734,该保留区间可能已经超过了保留的极限(如下)*/ 12 13 protected int marklimit;/*该成员变量表示了上面提到的标记最大保留区间大小,当pos-markpos> marklimit时,mark标记可能会被清除(根据实现确定)。*/
通过构造函数可以看到:初始化了一个byte数组作为缓冲区域
1 public BufferedInputStream(InputStream in, int size) { 2 super(in); 3 if (size <= 0) { 4 throw new IllegalArgumentException("Buffer size <= 0"); 5 } 6 buf = new byte[size]; 7 }
这个类中最为重要的方法是fill()方法,它提供了缓冲区域的读取、写入、区域元素的移动更新等。下面着重分析一下该方法:
1 private void fill() throws IOException { 2 byte[] buffer = getBufIfOpen(); 3 if (markpos < 0) { 4 /*如果不存在标记位置(即没有需要进行reset的位置需求) 5 则可以进行大胆地直接重置pos标识下一可读取位置,但是这样 6 不是会读取到以前的旧数据吗?不用担心,在后面的代码里☆会实现输入流的新 7 数据填充*/ 8 pos = 0; 9 }else if (pos >= buffer.length){ 10 /* 位置大于缓冲区长度,这里表示已经没有可用空间了 */ 11 if (markpos > 0) { 12 /* 表示存在mark位置,则要对mark位置到pos位置的数据予以保留, 13 以确保后面如果调用reset()重新从mark位置读取会取得成功*/ 14 int sz = pos - markpos; 15 /*该实现是通过将缓冲区域中markpos至pos部分的移至缓冲区头部实现*/ 16 System.arraycopy(buffer, markpos, buffer, 0, sz); 17 pos = sz; 18 markpos = 0; 19 } else if (buffer.length >= marklimit) { 20 /* 如果缓冲区已经足够大,可以容纳marklimit,则直接重置*/ 21 markpos = -1; 22 pos = 0;/* 丢弃所有的缓冲区内容 */ 23 } else { 24 /* 如果缓冲区还能增长的空间,则进行缓冲区扩容*/ 25 int nsz = pos * 2; 26 /*新的缓冲区大小设置成满足最大标记极限即可*/ 27 if (nsz > marklimit) 28 nsz = marklimit; 29 byte nbuf[] = new byte[nsz]; 30 //将原来的较小的缓冲内容COPY至增容的新缓冲区中 31 System.arraycopy(buffer, 0, nbuf, 0, pos); 32 //这里使用了原子变量引用更新,确保多线程环境下内存的可见性 33 if (!bufUpdater.compareAndSet(this, buffer, nbuf)) { 34 // Can't replace buf if there was an async close. 35 // Note: This would need to be changed if fill() 36 // is ever made accessible to multiple threads. 37 // But for now, the only way CAS can fail is via close. 38 // assert buf == null; 39 throw new IOException("Stream closed"); 40 } 41 buffer = nbuf; 42 } 43 count = pos; 44 //从原始输入流中读取数据,填充缓冲区 45 int n = getInIfOpen().read(buffer, pos, buffer.length - pos); 46 //根据实际读取的字节数更新缓冲区中可用字节数 47 if (n > 0) 48 count = n + pos; 49 }
整个fill的过程,可以看作是BufferedInputStream对外提供滑动读取的功能实现,通过预先读入一整段原始输入流数据至缓冲区中,而外界对BufferedInputStream的读取操作实际上是在缓冲区上进行,如果读取的数据超过了缓冲区的范围,那么BufferedInputStream负责重新从原始输入流中载入下一截数据填充缓冲区,然后外界继续通过缓冲区进行数据读取。这样的设计的好处是:避免了大量的磁盘IO,因为原始的InputStream类实现的read是即时读取的,即每一次读取都会是一次磁盘IO操作(哪怕只读取了1个字节的数据),可想而知,如果数据量巨大,这样的磁盘消耗非常可怕。而通过缓冲区的实现,读取可以读取缓冲区中的内容,当读取超过缓冲区的内容后再进行一次磁盘IO,载入一段数据填充缓冲,那么下一次读取一般情况下就直接可以从缓冲区读取,减少了磁盘IO。减少的磁盘IO大致可以通过以下方式计算(限read()方式):
length 流的最终大小
bufSize 缓冲区大小
则通过缓冲区实现的输入流BufferedInputStream的磁盘IO数为原始InputStream磁盘IO的
1/(length/bufSize)
read方法解析:该方法返回当前位置的后一位置byte值(int表示).
1 public synchronized int read() throws IOException { 2 if (pos >= count) { 3 /*表示读取位置已经超过了缓冲区可用范围,则对缓冲区进行重新填充*/ 4 fill(); 5 /*当填充后再次读取时发现没有数据可读,证明读到了流末尾*/ 6 if (pos >= count) 7 return -1; 8 } 9 /*这里表示读取位置尚未超过缓冲区有效范围,直接返回缓冲区内容*/ 10 return getBufIfOpen()[pos++] & 0xff; 11 }
一次读取多个字节(尽量读,非贪婪)
1 private int read1(byte[] b, int off, int len) throws IOException { 2 int avail = count - pos; 3 if (avail <= 0) { 4 /*这里使用了一个巧妙的机制,如果读取的长度大于缓冲区的长度 5 并且没有markpos,则直接从原始输入流中进行读取,从而避免无谓的 6 COPY(从原始输入流至缓冲区,读取缓冲区全部数据,清空缓冲区, 7 重新填入原始输入流数据)*/ 8 if (len >= getBufIfOpen().length && markpos < 0) { 9 return getInIfOpen().read(b, off, len); 10 } 11 /*当无数据可读时,从原始流中载入数据到缓冲区中*/ 12 fill(); 13 avail = count - pos; 14 if (avail <= 0) return -1; 15 } 16 int cnt = (avail < len) ? avail : len; 17 /*从缓冲区中读取数据,返回实际读取到的大小*/ 18 System.arraycopy(getBufIfOpen(), pos, b, off, cnt); 19 pos += cnt; 20 return cnt; 21 }
以下方法和上面的方法类似,唯一不同的是,上面的方法是尽量读,读到多少是多少,而下面的方法是贪婪的读,没有读到足够多的数据(len)就不会返回,除非读到了流的末尾。该方法通过不断循环地调用上面read1方法实现贪婪读取
1 public synchronized int read(byte b[], int off, int len) 2 throws IOException 3 { 4 getBufIfOpen(); // Check for closed stream 5 if ((off | len | (off + len) | (b.length - (off + len))) < 0) { 6 throw new IndexOutOfBoundsException(); 7 } else if (len == 0) { 8 return 0; 9 } 10 11 int n = 0; 12 for (;;) { 13 int nread = read1(b, off + n, len - n); 14 if (nread <= 0) 15 return (n == 0) ? nread : n; 16 n += nread; 17 if (n >= len) 18 return n; 19 // if not closed but no bytes available, return 20 InputStream input = in; 21 if (input != null && input.available() <= 0) 22 return n; 23 } 24 }
略过多少字节
1 public synchronized long skip(long n) throws IOException { 2 getBufIfOpen(); // Check for closed stream 3 if (n <= 0) { 4 return 0; 5 } 6 long avail = count - pos; 7 8 if (avail <= 0) { 9 // If no mark position set then don't keep in buffer 10 //从上面的注释可以知道,这也是一个巧妙的方法,如果没有mark标记, 11 // 则直接从原始输入流中skip 12 if (markpos <0) 13 return getInIfOpen().skip(n); 14 15 // Fill in buffer to save bytes for reset 16 fill(); 17 avail = count - pos; 18 if (avail <= 0) 19 return 0; 20 } 21 //该方法的实现为尽量原则,不保证一定略过规定的字节数。 22 long skipped = (avail < n) ? avail : n; 23 pos += skipped; 24 return skipped; 25 }
估计目前可用的字节数,原始流中可用的字节数+缓冲区中可用的字节数
1 public synchronized int available() throws IOException { 2 return getInIfOpen().available() + (count - pos); 3 }
标记位置:
1 public synchronized void reset() throws IOException { 2 getBufIfOpen(); // Cause exception if closed 3 if (markpos < 0) 4 throw new IOException("Resetting to invalid mark"); 5 pos = markpos; 6 }
重置位置:该实现清晰的表明下一读取位置被推到了以前的标记位置,以实现重新读取区段的功能
1 public synchronized void reset() throws IOException { 2 getBufIfOpen(); // Cause exception if closed 3 if (markpos < 0) 4 throw new IOException("Resetting to invalid mark"); 5 pos = markpos; 6 }
关闭流:首先通过线程安全的方式设置了内部的缓冲区引用为空,然后再对原始输入流进行关闭。
1 public void close() throws IOException { 2 byte[] buffer; 3 while ( (buffer = buf) != null) { 4 if (bufUpdater.compareAndSet(this, buffer, null)) { 5 InputStream input = in; 6 in = null; 7 if (input != null) 8 input.close(); 9 return; 10 } 11 // Else retry in case a new buf was CASed in fill() 12 } 13 }