系统源码分析-系统缩略图小文件系统MiniThumbFile

概述

前些日子看张绍文的高手课 / 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

  1. 保存 saveMiniThumbToFile

    	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位置。

  2. 读取 getMiniThumbFromFile

    	/**
         * 根据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定位存储位置,然后校验头信息,最后取出缩略图数据。

  3. 删除eraseMiniThumb

    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 - 文件"映射关系,将能连续访问的小文件合并存储,将原本小文件间的随机访问变为了顺序访问。

同时,合并存储能够有效减少小文件存储时所产生的磁盘碎片问题,提高磁盘的利用率。


参考

原生Android缩略图填满SD卡的问题

跨进程文件锁 - FileChannel

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值