Android 开源项目DiskLruCache 详解

有兴趣的同学可以读完这篇文章以后 可以看看这个硬盘缓存和volley 或者是其他 图片缓存框架中使用的硬盘缓存有什么异同点。

讲道理的话,其实硬盘缓存这个模块并不难写,难就难在 你要考虑到百分之0.1的那种情况,比如写文件的时候 手机突然没电了

之类的,你得保证文件正确性,唯一性等等。今天就来看看这个DiskLruCache是怎么实现这些内容的。

用法大家就自己去谷歌吧,在这里提一句,DiskLruCache 在4.0以上的源码中被编译到了platform 下面的libcore.io这个包路径下

所以你们看的那些博客如果告诉你 要把这个DiskLruCache 放在自己app下的libcore.io下 这是错的。因为你这么做,你自己app的类

和platform里面的类就重复了,你在运行以后,虽然不会报错,功能也正常,但实际上代码是不会走你app包路径下的DiskLruCache的。

他走的是platform 下面的,这一点一定要注意,不要被很多不负责任的博客坑了。。你就随便放在一个包路径下就可以了,只要不是

libcore.io这个路径下。另外自己可以先分析下这个DiskLruCache的日志 可以加深对这篇文章的理解,比如这种

libcore.io.DiskLruCache
1
1
1

DIRTY e37775b7868532e0d2986b1ff384c078
CLEAN e37775b7868532e0d2986b1ff384c078 152313

我们先来看看这个类的open函数,也是初始化的关键

 1 public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
 2             throws IOException {
 3
 4         if (maxSize <= 0) {
 5             throw new IllegalArgumentException("maxSize <= 0");
 6         }
 7         if (valueCount <= 0) {
 8             throw new IllegalArgumentException("valueCount <= 0");
 9         }
10
11         // 看备份文件是否存在
12         File backupFile = new File(directory, JOURNAL_FILE_BACKUP);
13         //如果备份文件存在,而正经的文件 不存在的话 就把备份文件 重命名为正经的journal文件
14         //如果正经的journal文件存在 那就把备份文件删除掉。
15         if (backupFile.exists()) {
16             File journalFile = new File(directory, JOURNAL_FILE);
17             if (journalFile.exists()) {
18                 backupFile.delete();
19             } else {
20                 renameTo(backupFile, journalFile, false);
21             }
22         }
23
24         //这个构造函数 无非就是 把值赋给相应的对象罢了
25         DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
27         //如果这个日志文件存在的话 就开始读里面的信息并返回
28         //主要就是构建entry列表
29         if (cache.journalFile.exists()) {
31             try {
32                 cache.readJournal();
33                 cache.processJournal();
34                 return cache;
35             } catch (IOException journalIsCorrupt) {
36                 System.out
37                         .println("DiskLruCache "
38                                 + directory
39                                 + " is corrupt: "
40                                 + journalIsCorrupt.getMessage()
41                                 + ", removing");
42                 cache.delete();
43             }
44         }
45
46         //如果日志文件不存在 就新建
47         directory.mkdirs();
48         cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
49         cache.rebuildJournal();
50         return cache;
51     }

这个open函数 其实还是挺好理解的。我们主要分两条线来看,一条线是 如果journal这个日志文件存在的话 就直接去构建entry列表。如果不存在 就去构建日志文件。

我们先来看 构建文件的这条线:

看49行 其实主要是调用了这个函数来完成构建。

 1  //这个就是我们可以直接在disk里面看到的journal文件 主要就是对他的操作
 2     private final File journalFile;
 3     //journal文件的temp 缓存文件,一般都是先构建这个缓存文件,等待构建完成以后将这个缓存文件重新命名为journal
 4     private final File journalFileTmp;
 5
 6 private synchronized void rebuildJournal() throws IOException {
 7         if (journalWriter != null) {
 8             journalWriter.close();
 9         }
10
11         //这个地方要注意了 writer 是指向的journalFileTmp 这个日志文件的缓存文件
12         Writer writer = new BufferedWriter(
13                 new OutputStreamWriter(new FileOutputStream(journalFileTmp), Util.US_ASCII));
14         //写入日志文件的文件头
15         try {
16             writer.write(MAGIC);
17             writer.write("\n");
18             writer.write(VERSION_1);
19             writer.write("\n");
20             writer.write(Integer.toString(appVersion));
21             writer.write("\n");
22             writer.write(Integer.toString(valueCount));
23             writer.write("\n");
24             writer.write("\n");
25
26             for (Entry entry : lruEntries.values()) {
27                 if (entry.currentEditor != null) {
28                     writer.write(DIRTY + ‘ ‘ + entry.key + ‘\n‘);
29                 } else {
30                     writer.write(CLEAN + ‘ ‘ + entry.key + entry.getLengths() + ‘\n‘);
31                 }
32             }
33         } finally {
34             writer.close();
35         }
36
37         if (journalFile.exists()) {
38             renameTo(journalFile, journalFileBackup, true);
39         }
40         //所以这个地方 构建日志文件的流程主要就是先构建出日志文件的缓存文件,如果缓存构建成功 那就直接重命名这个缓存文件
41         //可以想想这么做有什么好处
42         renameTo(journalFileTmp, journalFile, false);
43         journalFileBackup.delete();
44
45         //这里也是把写入日志文件的writer初始化
46         journalWriter = new BufferedWriter(
47                 new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII));
48     }

这条线 我们分析完毕以后 再来看看如果open的时候 缓存文件存在的时候 做了哪些操作。

回到open函数,看25-35行 发现是先调用的readJournalLine函数,然后调用了processJournal函数。

 1  private void readJournal() throws IOException {
 2         //StrictLineReader 这个类挺好用的,大家可以拷出来,这个类的源码大家可以自己分析 不难 以后还可以自己用
 3         StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile), Util.US_ASCII);
 4         try {
 5             //这一段就是读文件头的信息
 6             String magic = reader.readLine();
 7             String version = reader.readLine();
 8             String appVersionString = reader.readLine();
 9             String valueCountString = reader.readLine();
10             String blank = reader.readLine();
11             if (!MAGIC.equals(magic)
12                     || !VERSION_1.equals(version)
13                     || !Integer.toString(appVersion).equals(appVersionString)
14                     || !Integer.toString(valueCount).equals(valueCountString)
15                     || !"".equals(blank)) {
16                 throw new IOException("unexpected journal header: [" + magic + ", " + version + ", "
17                         + valueCountString + ", " + blank + "]");
18             }
19
20             //从这边开始 就要开始读下面的日志信息了 前面的都是日志头
21             int lineCount = 0;
22             //利用读到文件末尾的异常来跳出循环
23             while (true) {
24                 try {
25                     //就是在这里构建的lruEntries entry列表的
26                     readJournalLine(reader.readLine());
27                     lineCount++;
28                 } catch (EOFException endOfJournal) {
29                     break;
30                 }
31             }
32             redundantOpCount = lineCount - lruEntries.size();
33
34             // If we ended on a truncated line, rebuild the journal before appending to it.
35             if (reader.hasUnterminatedLine()) {
36                 rebuildJournal();
37             } else {
38                 //在这里把写入日志文件的Writer 初始化
39                 journalWriter = new BufferedWriter(new OutputStreamWriter(
40                         new FileOutputStream(journalFile, true), Util.US_ASCII));
41             }
42         } finally {
43             Util.closeQuietly(reader);
44         }
45     }

然后给你们看下这个函数里 主要的几个变量:

1  //每个entry对应的缓存文件的格式 一般为1
2     private final int valueCount;
3     private long size = 0;
4     //这个是专门用于写入日志文件的writer
5     private Writer journalWriter;
6     private final LinkedHashMap<String, Entry> lruEntries =
7             new LinkedHashMap<String, Entry>(0, 0.75f, true);
8     //这个值大于一定数目时 就会触发对journal文件的清理了
9     private int redundantOpCount;
 1 private final class Entry {
 2         private final String key;
 3
 4         /**
 5          * Lengths of this entry‘s files.
 6          * 这个entry中 每个文件的长度,这个数组的长度为valueCount 一般都是1
 7          */
 8         private final long[] lengths;
 9
10         /**
11          * True if this entry has ever been published.
12          * 曾经被发布过 那他的值就是true
13          */
14         private boolean readable;
15
16         /**
17          * The ongoing edit or null if this entry is not being edited.
18          * 这个entry对应的editor
19          */
20         private Editor currentEditor;
21
22         @Override
23         public String toString() {
24             return "Entry{" +
25                     "key=‘" + key + ‘\‘‘ +
26                     ", lengths=" + Arrays.toString(lengths) +
27                     ", readable=" + readable +
28                     ", currentEditor=" + currentEditor +
29                     ", sequenceNumber=" + sequenceNumber +
30                     ‘}‘;
31         }
32
33         /**
34          * The sequence number of the most recently committed edit to this entry.
35          * 最近编辑他的序列号
36          */
37         private long sequenceNumber;
38
39         private Entry(String key) {
40             this.key = key;
41             this.lengths = new long[valueCount];
42         }
43
44         public String getLengths() throws IOException {
45             StringBuilder result = new StringBuilder();
46             for (long size : lengths) {
47                 result.append(‘ ‘).append(size);
48             }
49             return result.toString();
50         }
51
52         /**
53          * Set lengths using decimal numbers like "10123".
54          */
55         private void setLengths(String[] strings) throws IOException {
56             if (strings.length != valueCount) {
57                 throw invalidLengths(strings);
58             }
59
60             try {
61                 for (int i = 0; i < strings.length; i++) {
62                     lengths[i] = Long.parseLong(strings[i]);
63                 }
64             } catch (NumberFormatException e) {
65                 throw invalidLengths(strings);
66             }
67         }
68
69         private IOException invalidLengths(String[] strings) throws IOException {
70             throw new IOException("unexpected journal line: " + java.util.Arrays.toString(strings));
71         }
72
73         //臨時文件創建成功了以後 就會重命名為正式文件了
74         public File getCleanFile(int i) {
75             Log.v("getCleanFile","getCleanFile path=="+new File(directory, key + "." + i).getAbsolutePath());
76             return new File(directory, key + "." + i);
77         }
78
79         //tmp开头的都是临时文件
80         public File getDirtyFile(int i) {
81             Log.v("getDirtyFile","getDirtyFile path=="+new File(directory, key + "." + i + ".tmp").getAbsolutePath());
82             return new File(directory, key + "." + i + ".tmp");
83         }
84
85
86     }

好,到了这里,我们DiskLruCache的open函数的主要流程就基本走完了,那么就再走2个流程结束本篇的源码分析,当然了,一个是GET操作,一个是SAVE操作了。

我们先看get操作

 1 //通过key 来取 该key对应的snapshot
 2     public synchronized Snapshot get(String key) throws IOException {
 3         checkNotClosed();
 4         validateKey(key);
 5         Entry entry = lruEntries.get(key);
 6         if (entry == null) {
 7             return null;
 8         }
 9
10         if (!entry.readable) {
11             return null;
12         }
13
14         // Open all streams eagerly to guarantee that we see a single published
15         // snapshot. If we opened streams lazily then the streams could come
16         // from different edits.
17         InputStream[] ins = new InputStream[valueCount];
18         try {
19             for (int i = 0; i < valueCount; i++) {
20                 ins[i] = new FileInputStream(entry.getCleanFile(i));
21             }
22         } catch (FileNotFoundException e) {
23             // A file must have been deleted manually!
24             for (int i = 0; i < valueCount; i++) {
25                 if (ins[i] != null) {
26                     Util.closeQuietly(ins[i]);
27                 } else {
28                     break;
29                 }
30             }
31             return null;
32         }
33
34         redundantOpCount++;
35         //在取得需要的文件以后 记得在日志文件里增加一条记录 并检查是否需要重新构建日志文件
36         journalWriter.append(READ + ‘ ‘ + key + ‘\n‘);
37         if (journalRebuildRequired()) {
38             executorService.submit(cleanupCallable);
39         }
40
41         return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths);
42     }

看第四行的那个函数:

1  private void validateKey(String key) {
2         Matcher matcher = LEGAL_KEY_PATTERN.matcher(key);
3         if (!matcher.matches()) {
4             throw new IllegalArgumentException("keys must match regex "
5                     + STRING_KEY_PATTERN + ": \"" + key + "\"");
6         }
7     }

实际上我们在这里就能发现 存储entry的map的key 就是在这里被验证的,实际上就是正则表达式的验证,所以我们在使用这个cache的时候

key一定要用md5加密,因为图片的url一般都会有特殊字符,是不符合这里的验证的。

然后看37-39行:实际上就是走的这里:

 1  final ThreadPoolExecutor executorService =
 2             new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
 3     private final Callable<Void> cleanupCallable = new Callable<Void>() {
 4         public Void call() throws Exception {
 5             synchronized (DiskLruCache.this) {
 6                 if (journalWriter == null) {
 7                     return null; // Closed.
 8                 }
 9                 trimToSize();
10                 if (journalRebuildRequired()) {
11                     rebuildJournal();
12                     redundantOpCount = 0;
13                 }
14             }
15             return null;
16         }
17     };

这边就是分两个部分,一个是校验 总缓存大小是否超出了限制的数量,另外一个10-13行 就是校验 我们的操作数redundantOpCount 是否超出了范围,否则就重构日志文件。

1 private boolean journalRebuildRequired() {
2         final int redundantOpCompactThreshold = 2000;
3         return redundantOpCount >= redundantOpCompactThreshold //
4                 && redundantOpCount >= lruEntries.size();
5     }

最后我们回到get函数看最后一行 发现返回的是一个SnapShot,快照对象

 1  /**
 2      * A snapshot of the values for an entry.
 3      * 这个类持有该entry中每个文件的inputStream 通过这个inputStream 可以读取他的内容
 4      */
 5     public final class Snapshot implements Closeable {
 6         private final String key;
 7         private final long sequenceNumber;
 8         private final InputStream[] ins;
 9         private final long[] lengths;
10
11         private Snapshot(String key, long sequenceNumber, InputStream[] ins, long[] lengths) {
12             this.key = key;
13             this.sequenceNumber = sequenceNumber;
14             this.ins = ins;
15             this.lengths = lengths;
16         }
17
18         /**
19          * Returns an editor for this snapshot‘s entry, or null if either the
20          * entry has changed since this snapshot was created or if another edit
21          * is in progress.
22          */
23         public Editor edit() throws IOException {
24             return DiskLruCache.this.edit(key, sequenceNumber);
25         }
26
27         /**
28          * Returns the unbuffered stream with the value for {@code index}.
29          */
30         public InputStream getInputStream(int index) {
31             return ins[index];
32         }
33
34         /**
35          * Returns the string value for {@code index}.
36          */
37         public String getString(int index) throws IOException {
38             return inputStreamToString(getInputStream(index));
39         }
40
41         /**
42          * Returns the byte length of the value for {@code index}.
43          */
44         public long getLength(int index) {
45             return lengths[index];
46         }
47
48         public void close() {
49             for (InputStream in : ins) {
50                 Util.closeQuietly(in);
51             }
52         }
53     }

到这里就明白了get最终返回的其实就是entry根据key 来取的snapshot对象,这个对象直接把inputStream暴露给外面。

最后我们再看看save的过程 先取得editor

 1  public Editor edit(String key) throws IOException {
 2         return edit(key, ANY_SEQUENCE_NUMBER);
 3     }
 4
 5     //根据传进去的key 创建一个entry 并且将这个key加入到entry的那个map里 然后创建一个对应的editor
 6     //同时在日志文件里加入一条对该key的dirty记录
 7     private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
 8         //因为这里涉及到写文件 所以要先校验一下写日志文件的writer 是否被正确的初始化
 9         checkNotClosed();
10         //这个地方是校验 我们的key的,通常来说 假设我们要用这个缓存来存一张图片的话,我们的key 通常是用这个图片的
11         //网络地址 进行md5加密,而对这个key的格式在这里是有要求的 所以这一步就是验证key是否符合规范
12         validateKey(key);
13         Entry entry = lruEntries.get(key);
14         if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
15                 || entry.sequenceNumber != expectedSequenceNumber)) {
16             return null; // Snapshot is stale.
17         }
18         if (entry == null) {
19             entry = new Entry(key);
20             lruEntries.put(key, entry);
21         } else if (entry.currentEditor != null) {
22             return null; // Another edit is in progress.
23         }
24
25         Editor editor = new Editor(entry);
26         entry.currentEditor = editor;
27
28         // Flush the journal before creating files to prevent file leaks.
29         journalWriter.write(DIRTY + ‘ ‘ + key + ‘\n‘);
30         journalWriter.flush();
31         return editor;
32     }

然后取得输出流

 public OutputStream newOutputStream(int index) throws IOException {
            if (index < 0 || index >= valueCount) {
                throw new IllegalArgumentException("Expected index " + index + " to "
                        + "be greater than 0 and less than the maximum value count "
                        + "of " + valueCount);
            }
            synchronized (DiskLruCache.this) {
                if (entry.currentEditor != this) {
                    throw new IllegalStateException();
                }
                if (!entry.readable) {
                    written[index] = true;
                }
                File dirtyFile = entry.getDirtyFile(index);

                FileOutputStream outputStream;
                try {
                    outputStream = new FileOutputStream(dirtyFile);
                } catch (FileNotFoundException e) {
                    // Attempt to recreate the cache directory.
                    directory.mkdirs();
                    try {
                        outputStream = new FileOutputStream(dirtyFile);
                    } catch (FileNotFoundException e2) {
                        // We are unable to recover. Silently eat the writes.
                        return NULL_OUTPUT_STREAM;
                    }
                }
                return new FaultHidingOutputStream(outputStream);
            }
        }

注意这个index 其实一般传0 就可以了,DiskLruCache 认为 一个key 下面可以对应多个文件,这些文件 用一个数组来存储,所以正常情况下 我们都是

一个key 对应一个缓存文件 所以传0

1 //tmp开头的都是临时文件
2         public File getDirtyFile(int i) {
4             return new File(directory, key + "." + i + ".tmp");
5         }

然后你这边就能看到,这个输出流,实际上是tmp 也就是缓存文件的 .tmp 也就是缓存文件的  缓存文件 输出流。

这个流 我们写完毕以后 就要commit

 1   public void commit() throws IOException {
 2             if (hasErrors) {
 3                 completeEdit(this, false);
 4                 remove(entry.key); // The previous entry is stale.
 5             } else {
 6                 completeEdit(this, true);
 7             }
 8             committed = true;
 9         }
10 /这个就是根据缓存文件的大小 更新disklrucache的总大小 然后再日志文件里对该key加入clean的log
11     //最后判断是否超过最大的maxSize 以便对缓存进行清理
12     private synchronized void completeEdit(Editor editor, boolean success) throws IOException {
13         Entry entry = editor.entry;
14         if (entry.currentEditor != editor) {
15             throw new IllegalStateException();
16         }
17
18         // If this edit is creating the entry for the first time, every index must have a value.
19         if (success && !entry.readable) {
20             for (int i = 0; i < valueCount; i++) {
21                 if (!editor.written[i]) {
22                     editor.abort();
23                     throw new IllegalStateException("Newly created entry didn‘t create value for index " + i);
24                 }
25                 if (!entry.getDirtyFile(i).exists()) {
26                     editor.abort();
27                     return;
28                 }
29             }
30         }
31
32         for (int i = 0; i < valueCount; i++) {
33             File dirty = entry.getDirtyFile(i);
34             if (success) {
35                 if (dirty.exists()) {
36                     File clean = entry.getCleanFile(i);
37                     dirty.renameTo(clean);
38                     long oldLength = entry.lengths[i];
39                     long newLength = clean.length();
40                     entry.lengths[i] = newLength;
41                     size = size - oldLength + newLength;
42                 }
43             } else {
44                 deleteIfExists(dirty);
45             }
46         }
47
48         redundantOpCount++;
49         entry.currentEditor = null;
50         if (entry.readable | success) {
51             entry.readable = true;
52             journalWriter.write(CLEAN + ‘ ‘ + entry.key + entry.getLengths() + ‘\n‘);
53             if (success) {
54                 entry.sequenceNumber = nextSequenceNumber++;
55             }
56         } else {
57             lruEntries.remove(entry.key);
58             journalWriter.write(REMOVE + ‘ ‘ + entry.key + ‘\n‘);
59         }
60         journalWriter.flush();
61
62         if (size > maxSize || journalRebuildRequired()) {
63             executorService.submit(cleanupCallable);
64         }
65     }

大家看那个32-40行,就是你commit以后 就会把tmp文件转正 ,重命名为 真正的缓存文件了。

这个里面的流程和日志文件的rebuild 是差不多的,都是为了防止写文件的出问题。所以做了这样的冗余处理。

基本上到这就结束了,大家主要通过这个框架可以学习到一些文件读写操作 的知识 ,另外可以看一下

硬盘缓存到底是怎么做的,大概需要一个什么样的流程,以后做到微博类的应用的时候 也可以快速写出一个硬盘缓存(非缓存图片的)

时间: 2024-10-13 06:48:56

Android 开源项目DiskLruCache 详解的相关文章

开源项目MultiChoiceAdapter详解(五)——可扩展的MultiChoiceBaseAdapter

上次写到了开源项目MultiChoiceAdapter详解(四)——MultiChoiceBaseAdapter的使用,其实我们仍旧可以不使用ActionMode的,所以这里就写一个自己扩展的方法. 一.布局文件 listview_normal_layout.xml <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.andr

开源项目MultiChoiceAdapter详解(六)——GridView和MultiChoiceBaseAdapter配合使用

这篇其实没啥重要的,主要就算是个总结吧. 一. 这里实现的是类似于上图的多图选择的效果.关键在于item布局文件的写法.这也就是这个框架奇葩的一点,莫名其妙的要在一个自定义控件里面再放一个自定义的控件,如果不这样就出不了选中的效果.分析下原因是这里整个item被图片所覆盖了,仅仅设置一个有选择效果的父控件会被图片所覆盖,所以还得用一个可以选中的iamgeview进行替换imageview. 下面就是这个布局文件 item_gridview.xml <?xml version="1.0&qu

开源项目使用详解过程

开源项目使用详解过程Q-Q:971-041-894定位[手機系列找回刪除等信息]这件事说来也是巧了,也算是他们上级领导的矛盾吧,因为公司与另一个公司之前有一点合同上的纠葛,所以老板在发函以前之前让我跟对方公司现承认一下,看是不是真的要闹到这个地步,因为我是担任这个项目的,相对在状况上回对比的了解,那个时分我刚好在外面,所以只能用自己的手机打以前了,这也没什么,不便是知会一声吗,当然的,两头的利益不是那么快就可以到达的,所以发函这个工作仍是进行了请问我的苹果5s手机被偷关还能机么定位吗.手机没电话

Android开源框架Universal-Image-Loader详解

如果说评价一下哪个图片开源库最被广泛使用的话,我想应该可以说是Universal-Image-Loader,在主流的应用中如 果你随便去反编译几个,基本都能看到他的身影,它就像个图片加载守护者,默默的守护着图片加载.相信很多人对 这个异步加载图片框架还不是很熟,再加上最近它更改优化了好几个地方,而网上的大部分资料还是以前的,于是花 了几天时间专门的研究了下开源框架Universal-Image-Loader(实际上是近期项目刚好用到,且仔细的考虑过各种情 况),希望对新手能有所帮助,也希望大神能

Android开源框架Image-Loader详解

如果说评价一下哪个图片开源库最被广泛使用的话,我想应该可以说是Universal-Image-Loader,在主流的应用中如 果你随便去反编译几个,基本都能看到他的身影,它就像个图片加载守护者,默默的守护着图片加载.相信很多人对 这个异步加载图片框架还不是很熟,再加上最近它更改优化了好几个地方,而网上的大部分资料还是以前的,于是花 了几天时间专门的研究了下开源框架Universal-Image-Loader(实际上是近期项目刚好用到,且仔细的考虑过各种情 况),希望对新手能有所帮助,也希望大神能

开源项目PullToRefresh详解(二)——PullToRefreshGridView

这里介绍的是PullToRefreshGridView的使用方法,和之前的PullToRefreshListView方法如出一辙,因为这个开源项目模块化很棒,所以很容易实现.等于说我们可以按照之前使用控件的方式来操作,不用考虑其他的问题. 思路: 1.写布局文件,放入可以下拉刷新的控件 2.找到下拉刷新的控件,设置监听器,并且在刷新方法中开启一个异步任务来操作 3.通过这个下拉刷新控件的getRefreshableView()方法来得到GridView对象,按照正常的操作来设置适配器 4.在异步

开源项目MultiChoiceAdapter详解(三)——MulitChoiceNormalArrayAdapter的使用

MulitChoiceNormalArrayAdapter是我自己定义的一个类,其实就是实现了MulitChoiceArrayAdapter,为什么做这个简单的实现类呢,因为这样我们在不用ActionMode的时候就不用每次要写一个类来继承MulitChoiceArrayAdapter了,直接实例化MulitChoiceNormalArrayAdapter即可.下面贴一个compat包下的MulitChoiceNormalArrayAdapter的源码. MulitChoiceNormalArr

开源项目MultiChoiceAdapter详解(四)——MultiChoiceBaseAdapter的使用

MultiChoiceBaseAdapter是一个可以多选的BaseAdapter,使用的方式相比来说扩展性更强! 使用方式: 1.布局文件 2.写一个类继承MultiChoiceBaseAdapter 3.实现内部的各个方法 4.设置数据源和视图 5.完成保存的回调方法 一.布局文件 listview_actionmode_layout.xml <?xml version="1.0" encoding="utf-8"?> <LinearLayou

开源项目MultiChoiceAdapter详解(二)——MultiChoiceArrayAdapter的使用

MultiChoiceArrayAdapter其实就是可以多选的ArrayAdapter了,ArrayAdpter我们已经很熟悉了.MultiChoiceArrayAdapter这个类是抽象类,所以使用前必须要继承.下面是使用MultiChoiceArrayAdapter的步骤: 0.用自定义的控件来写一个layout 1.写一个类来继承MultiChoiceArrayAdapter 2.实例化这个类 3.用setAdapterView()来设置要加载适配器的控件. 4.写上保存的方法 @Ove