概述
前些日子看张绍文的高手课 / IO优化(中)时,里面提到了微信的一个小文件系统,但是没有细说,在评论区回答学员的问题时大概说了下原理:
作者回复: 这个小文件系统是应用层的方案,只是把一大堆的小文件组织成一个超大文件。并没有
替换原生的文件系统
笔者看到这块,突然想到很早之前看过的系统缩略图相关源码时,了解到的一个知识点跟其描述的很像,猜测大概就是类似的东西,随重新温习一下。
.thumbdata
做过相册相关的童鞋有可能知道,在sd卡相册目录下会有一个缩略图集合文件.thumbdata(/sdcard/DCIM/.thumbnails/.thumbdataXXX
),这个.thumbdata文件保存了系统扫描过的图片缩略图。
xxxxxxxx:/sdcard/DCIM/.thumbnails # ls -al
total 60
drwxrwx--x 2 root sdcard_rw 4096 2021-04-27 18:05 .
drwxrwx--x 5 root sdcard_rw 4096 2021-04-27 18:05 ..
-rw-rw---- 1 root sdcard_rw 5272489 2021-04-27 18:05 .thumbdata4--1967290299
简单来说就是系统会对sd卡上的文件进行扫描,得到文件的一些信息,并写入external.db
数据库的files表中,对于图片(如截图、照片等),系统会以其在files表中的_id值**(自增)**,作为定位标志,将其缩略图文件数据保存到.thumbdata文件中。
xxxxxxxx:/data/data/com.android.providers.media/databases # ls
external.db
ps:_id从20直接调到46,是因为21-45之间的记录被删除了。
1、这些缩略图有什么用呢?
其实就是为了在需要缩略图时,方便获取,节省内存,较少IO操作,提高性能。我们可以通过以下方法获得图片的缩略图:
//获取id为fileId的图片的缩略图,无则自动生成一个并返回。
MediaStore.Video.Thumbnails.getThumbnail(mAppContext.getContentResolver(), fileId, MediaStore.Video.Thumbnails.MICRO_KIND, null);
2、那些场景会用呢?
比如打开系统自带的相册,一个个小格子显示的图片就是从这个缩略图文件.thumbdata中获取来的。
MiniThumbFile
-
private static final int MINI_THUMB_DATA_FILE_VERSION = 4;//文件版本号 public static final int BYTES_PER_MINTHUMB = 10000;//某张缩略图预分配的大小 /** * 1 byte status (0 = empty, 1 = mini-thumb available) * 8 bytes magic (a magic number to match what's in the database) * 4 bytes data length (LEN) * LEN bytes jpeg data **/ private static final int HEADER_SIZE = 1 + 8 + 4;//头信息 /** * 保存缩略图信息到.thumbdata文件中 * @param data 图片byte数据,可通过ByteArrayOutputStream得到 * @param id files表中的_id值,一个自增字段 * @param magic 魔数,校验用的,其实是个随机数,对应files表中的mini_thumb_magic值 * @throws IOException */ 237 public synchronized void saveMiniThumbToFile(byte[] data, long id, long magic) 238 throws IOException { 239 RandomAccessFile r = miniThumbDataFile();//获取.thumbdata文件对象 240 if (r == null) return; 241 242 long pos = id * BYTES_PER_MINTHUMB;//根据id值,定位置 243 FileLock lock = null; 244 try { 245 if (data != null) { 246 if (data.length > BYTES_PER_MINTHUMB - HEADER_SIZE) { 247 // not enough space to store it. //缩略图图片真实大小,必须小于(去掉头)预置大小 248 return; 249 } 250 mBuffer.clear(); //保存头信息 251 mBuffer.put((byte) 1);//标记可获取 252 mBuffer.putLong(magic);//保存魔数 253 mBuffer.putInt(data.length);//图片真实大小 //保存图片数据 254 mBuffer.put(data);//正真的图片数据 255 mBuffer.flip();//类似flush 256 //锁住要写入的区域 257 lock = mChannel.lock(pos, BYTES_PER_MINTHUMB, false); //通过FileChannel写入文件指定位置 258 mChannel.write(mBuffer, pos); 259 } 260 } catch (IOException ex) { 261 Log.e(TAG, "couldn't save mini thumbnail data for " 262 + id + "; ", ex); 263 throw ex; 264 } catch (RuntimeException ex) { 265 // Other NIO related exception like disk full, read only channel..etc 266 Log.e(TAG, "couldn't save mini thumbnail data for " 267 + id + "; disk full or mount read-only? " + ex.getClass()); 268 } finally { 269 try { 270 if (lock != null) lock.release(); 271 } 272 catch (IOException ex) { 273 // ignore it. 274 } 275 } 276 } //获取.thumbdata文件的对象 mMiniThumbFile 101 private RandomAccessFile miniThumbDataFile() { 102 if (mMiniThumbFile == null) { //移除老版本的.thumbdata文件 103 removeOldFile(); //获取新版本.thumbdata文件路径名称 104 String path = randomAccessFilePath(MINI_THUMB_DATA_FILE_VERSION); ... mMiniThumbFile = new RandomAccessFile(f, "rw"); ... } 128 return mMiniThumbFile; 129 }
上面的代码很简单,就是给每一张缩略图分配了指定大小的空间
BYTES_PER_MINTHUMB
去存放信息,至于放到文件哪个位置,则根据唯一的_id去计算的。比如:A图的_id是0,则其存放起始位置是
0*BYTES_PER_MINTHUMB
,即.thumbdata
文件的开头。B图的_id是1,则其保存的起始位置是文件的1*BYTES_PER_MINTHUMB
位置。
-
/** * 根据id定位,获取缩略图 * @param id 根据id定位 * @param data buff * @return 返回data或null */ 285 public synchronized byte [] getMiniThumbFromFile(long id, byte [] data) { 286 RandomAccessFile r = miniThumbDataFile(); 287 if (r == null) return null; 288 //定位 289 long pos = id * BYTES_PER_MINTHUMB; 290 FileLock lock = null; 291 try { 292 mBuffer.clear(); 293 lock = mChannel.lock(pos, BYTES_PER_MINTHUMB, true); 294 int size = mChannel.read(mBuffer, pos); 295 if (size > 1 + 8 + 4) { // flag, magic, length 296 mBuffer.position(0); //读出头信息 297 byte flag = mBuffer.get(); 298 long magic = mBuffer.getLong(); 299 int length = mBuffer.getInt(); 300 //判断 301 if (size >= 1 + 8 + 4 + length && length != 0 && magic != 0 && flag == 1 && 302 data.length >= length) { 303 mBuffer.get(data, 0, length); 304 return data; 305 } 306 } 307 } catch (IOException ex) { 308 Log.w(TAG, "got exception when reading thumbnail id=" + id + ", exception: " + ex); 309 } catch (RuntimeException ex) { 310 // Other NIO related exception like disk full, read only channel..etc 311 Log.e(TAG, "Got exception when reading thumbnail, id = " + id + 312 ", disk full or mount read-only? " + ex.getClass()); 313 } finally { 314 try { 315 if (lock != null) lock.release(); 316 } 317 catch (IOException ex) { 318 // ignore it. 319 } 320 } 321 return null; 322 }
如果你已经熟悉
saveMiniThumbToFile
的代码,那么这块读也就很容易理解了,里面关键位置都有注释。简述一下就是,通过_id定位存储位置,然后校验头信息,最后取出缩略图数据。
-
189 public synchronized void eraseMiniThumb(long id) { 190 RandomAccessFile r = miniThumbDataFile(); 191 if (r != null) { //定位 192 long pos = id * BYTES_PER_MINTHUMB; 193 FileLock lock = null; 194 try { 195 mBuffer.clear(); 196 mBuffer.limit(1 + 8); 197 198 lock = mChannel.lock(pos, BYTES_PER_MINTHUMB, false); 199 // check that we can read the following 9 bytes 200 // (1 for the "status" and 8 for the long) //读出: status (1 byte ) + magic (8 bytes) 201 if (mChannel.read(mBuffer, pos) == 9) { 202 mBuffer.position(0); 203 if (mBuffer.get() == 1) { 204 long currentMagic = mBuffer.getLong(); 205 if (currentMagic == 0) { 206 // there is no thumbnail stored here 207 Log.i(TAG, "no thumbnail for id " + id); 208 return; 209 } 210 // zero out the thumbnail slot 211 // Log.v(TAG, "clearing slot " + id + ", magic " + currentMagic 212 // + " at offset " + pos); //全部置位0 213 mChannel.write(mEmptyBuffer, pos); 214 } 215 } else { 216 // Log.v(TAG, "No slot"); 217 } ... 235 }
进阶思考
有这么一种情况,假设有1万张图片,其_id从1~10000,此时我们删除多余图片,最终只剩最后一张,其_id为10000。
那么,根据上述算法,其的缩略图保存的起始位置为.thumbdata文件的10000*BYTES_PER_MINTHUMB
。
此时,缩略图.thumbdata文件大小至少为10000*BYTES_PER_MINTHUMB
。
.thumbdata文件10000*BYTES_PER_MINTHUMB
之前的位置其实是空闲的,这就造成了很大的资源浪费。
我们有什么办法可以去优化呢?其实可以根据分页的思想来解决该问题。
即,可以将前1024张缩略图放入.thumbdata-0中,1025~2048张翻入.thumbdata-1以此类推,最终按1024来分页。
这样上面的情况下,最终也只会创建一个最大
1024*BYTES_PER_MINTHUMB
的缩略图文件。
/**简单修改,未做验证,谨慎尝试,抛砖引玉**/
public synchronized void saveMiniThumbToFile(byte[] data, long id, long magic)
throws IOException {
int page = id / (1024+1);
id = id % (1024 + 1);
...
}
public synchronized byte [] getMiniThumbFromFile(long id, byte [] data) {
int page = id / (1024+1);
id = id % (1024 + 1);
....
}
public synchronized void eraseMiniThumb(long id) {
int page = id / (1024+1);
id = id % (1024 + 1);
...
}
private String randomAccessFilePath(int version, int page) {
String directoryName =
Environment.getExternalStorageDirectory().toString()
+ "/DCIM/.thumbnails";
return directoryName + "/.thumbdata" + version + "-" + mUri.hashCode() + "-" + page;
}
总结
如果认真看完上面的内容后,我相信对于自己实现一个 “小文件系统”,应该是易于反掌了。
而且,对于大量的小文件合并为大文件,除了可以大大提高IO性能外。其内部使用了FileChannel
通过映射文件来操作文件,也能大幅提升性能。
我们还可以根据业务,通过"id - 文件"映射关系,将能连续访问的小文件合并存储,将原本小文件间的随机访问变为了顺序访问。
同时,合并存储能够有效减少小文件存储时所产生的磁盘碎片问题,提高磁盘的利用率。