加入收藏 | 设为首页 | 会员中心 | 我要投稿 北几岛 (https://www.beijidao.com.cn/)- 科技、建站、经验、云计算、5G、大数据,站长网!
当前位置: 首页 > 大数据 > 正文

安卓开发笔记——关于照片墙的实现(完美缓存策略LruCache+DiskL

发布时间:2021-07-06 06:06:08 所属栏目:大数据 来源: https://www.jb51.cc
导读:这几天一直研究在安卓开发中图片应该如何处理,在网上翻了好多资料,这里做点小总结,如果朋友们有更好的解决方案,可以留言一起交流下。 ? 内存缓存技术 在我们开发程序中要在界面上加载一张图片是件非常容易的事情,但如果是加载一堆图片呢?比如ListView

这几天一直研究在安卓开发中图片应该如何处理,在网上翻了好多资料,这里做点小总结,如果朋友们有更好的解决方案,可以留言一起交流下。

?

内存缓存技术

在我们开发程序中要在界面上加载一张图片是件非常容易的事情,但如果是加载一堆图片呢?比如ListView,GridView这类的控件,随着屏幕滑动,图片加载也会越来越多,应用程序所可以使用的内存毕竟是有限的,如果一味的去加载图片,很容易导致OOM(Out Of Memory)内存溢出,导致程序崩溃。

这里我们一般的做法是将显示在屏幕之外的图片进行内存回收,此时的垃圾回收器会认为应用对这些图片不再持有引用,从而进行GC操作。但现实还需要考虑到问题是,如果用户又滑动屏幕回到之前我们已经回收掉的图片位置,这时候该怎么办?重新去加载一张图片肯定是不可取的,这样既浪费了时间,又浪费了用户的流量。

这里我们就会想到利用内存缓存来解决这个问题,利用内存缓存可以让应用快速的加载和处理图片,从而提高流畅性。

内存缓存技术对那些大量占用应用程序宝贵内存的图片提供了快速访问的方法,其中最核心的类是LruCache (此类在android-support-v4的包中提供) 。这个类非常适合用来缓存图片,它的主要算法原理是把最近使用的对象用强引用存储在 LinkedHashMap 中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除。

在过去,我们经常会使用一种非常流行的内存缓存技术的实现,即软引用或弱引用 (SoftReference or WeakReference)。但是现在已经不再推荐使用这种方式了,因为从 Android 2.3 (API Level 9)开始,垃圾回收器会更倾向于回收持有软引用或弱引用的对象,这让软引用和弱引用变得不再可靠。另外,Android 3.0 (API Level 11)中,图片的数据会存储在本地的内存当中,因而无法用一种可预见的方式将其释放,这就有潜在的风险造成应用程序的内存溢出并崩溃。

对于LruCache类不熟悉的朋友可以看看这篇文章《Android高效加载大图、多图解决方案,有效避免程序OOM》。

?

磁盘缓存技术

对于内存缓存LruCache只是管理了内存中图片的存储与释放,如果图片从内存中被移除的话,那么又需要从网络上重新加载一次图片,这显然非常耗时。对此,Google又提供了一套硬盘缓存的解决方案:DiskLruCache(非Google官方编写,但获得官方认证)。

关于磁盘缓存DisLruCache类不熟悉的朋友可以看看这篇文章的介绍《Android DiskLruCache完全解析,硬盘缓存的最佳方案》  

?

?

?

完美结合LruCache+DiskLruCache

首先先来看下实现效果:

??

?

这是一个很简单的布局,大布局是GridView,小布局ImageView嵌套在大布局里,贴上代码再做分析吧,其实注释也挺全的。

既然要完成磁盘存储,那么必不可少的就是DisLruCache类的,先引进项目里再说,再来就是图片资源集合了。

图片资源类:

 1 package com.example.photoswall;
 2 
 3 
 4 /**
 5  * 图片资源类
 6  * @author Balla_兔子
 7  *
 8  */
 9 public class Images {
10 
11     final static String[] imageThumbUrls = new String[] {
12         "http://img.my.csdn.net/uploads/201407/26/1406383299_1976.jpg",13         "http://img.my.csdn.net/uploads/201407/26/1406383291_6518.jpg"14         "http://img.my.csdn.net/uploads/201407/26/1406383291_8239.jpg"15         "http://img.my.csdn.net/uploads/201407/26/1406383290_9329.jpg"16         "http://img.my.csdn.net/uploads/201407/26/1406383290_1042.jpg"17         "http://img.my.csdn.net/uploads/201407/26/1406383275_3977.jpg"18         "http://img.my.csdn.net/uploads/201407/26/1406383265_8550.jpg"19         "http://img.my.csdn.net/uploads/201407/26/1406383264_3954.jpg"20         "http://img.my.csdn.net/uploads/201407/26/1406383264_4787.jpg"21         "http://img.my.csdn.net/uploads/201407/26/1406383264_8243.jpg"22         "http://img.my.csdn.net/uploads/201407/26/1406383248_3693.jpg"23         "http://img.my.csdn.net/uploads/201407/26/1406383243_5120.jpg"24         "http://img.my.csdn.net/uploads/201407/26/1406383242_3127.jpg"25         "http://img.my.csdn.net/uploads/201407/26/1406383242_9576.jpg"26         "http://img.my.csdn.net/uploads/201407/26/1406383242_1721.jpg"27         "http://img.my.csdn.net/uploads/201407/26/1406383219_5806.jpg"28         "http://img.my.csdn.net/uploads/201407/26/1406383214_7794.jpg"29         "http://img.my.csdn.net/uploads/201407/26/1406383213_4418.jpg"30         "http://img.my.csdn.net/uploads/201407/26/1406383213_3557.jpg"31         "http://img.my.csdn.net/uploads/201407/26/1406383210_8779.jpg"32         "http://img.my.csdn.net/uploads/201407/26/1406383172_4577.jpg"33         "http://img.my.csdn.net/uploads/201407/26/1406383166_3407.jpg"34         "http://img.my.csdn.net/uploads/201407/26/1406383166_2224.jpg"35         "http://img.my.csdn.net/uploads/201407/26/1406383166_7301.jpg"36         "http://img.my.csdn.net/uploads/201407/26/1406383165_7197.jpg"37         "http://img.my.csdn.net/uploads/201407/26/1406383150_8410.jpg"38         "http://img.my.csdn.net/uploads/201407/26/1406383131_3736.jpg"39         "http://img.my.csdn.net/uploads/201407/26/1406383130_5094.jpg"40         "http://img.my.csdn.net/uploads/201407/26/1406383130_7393.jpg"41         "http://img.my.csdn.net/uploads/201407/26/1406383129_8813.jpg"42         "http://img.my.csdn.net/uploads/201407/26/1406383100_3554.jpg"43         "http://img.my.csdn.net/uploads/201407/26/1406383093_7894.jpg"44         "http://img.my.csdn.net/uploads/201407/26/1406383092_2432.jpg"45         "http://img.my.csdn.net/uploads/201407/26/1406383092_3071.jpg"46         "http://img.my.csdn.net/uploads/201407/26/1406383091_3119.jpg"47         "http://img.my.csdn.net/uploads/201407/26/1406383059_6589.jpg"48         "http://img.my.csdn.net/uploads/201407/26/1406383059_8814.jpg"49         "http://img.my.csdn.net/uploads/201407/26/1406383059_2237.jpg"50         "http://img.my.csdn.net/uploads/201407/26/1406383058_4330.jpg"51         "http://img.my.csdn.net/uploads/201407/26/1406383038_3602.jpg"52         "http://img.my.csdn.net/uploads/201407/26/1406382942_3079.jpg"53         "http://img.my.csdn.net/uploads/201407/26/1406382942_8125.jpg"54         "http://img.my.csdn.net/uploads/201407/26/1406382942_4881.jpg"55         "http://img.my.csdn.net/uploads/201407/26/1406382941_4559.jpg"56         "http://img.my.csdn.net/uploads/201407/26/1406382941_3845.jpg"57         "http://img.my.csdn.net/uploads/201407/26/1406382924_8955.jpg"58         "http://img.my.csdn.net/uploads/201407/26/1406382923_2141.jpg"59         "http://img.my.csdn.net/uploads/201407/26/1406382923_8437.jpg"60         "http://img.my.csdn.net/uploads/201407/26/1406382922_6166.jpg"61         "http://img.my.csdn.net/uploads/201407/26/1406382922_4843.jpg"62         "http://img.my.csdn.net/uploads/201407/26/1406382905_5804.jpg"63         "http://img.my.csdn.net/uploads/201407/26/1406382904_3362.jpg"64         "http://img.my.csdn.net/uploads/201407/26/1406382904_2312.jpg"65         "http://img.my.csdn.net/uploads/201407/26/1406382904_4960.jpg"66         "http://img.my.csdn.net/uploads/201407/26/1406382900_2418.jpg"67         "http://img.my.csdn.net/uploads/201407/26/1406382881_4490.jpg"68         "http://img.my.csdn.net/uploads/201407/26/1406382881_5935.jpg"69         "http://img.my.csdn.net/uploads/201407/26/1406382880_3865.jpg"70         "http://img.my.csdn.net/uploads/201407/26/1406382880_4662.jpg"71         "http://img.my.csdn.net/uploads/201407/26/1406382879_2553.jpg"72         "http://img.my.csdn.net/uploads/201407/26/1406382862_5375.jpg"73         "http://img.my.csdn.net/uploads/201407/26/1406382862_1748.jpg"74         "http://img.my.csdn.net/uploads/201407/26/1406382861_7618.jpg"75         "http://img.my.csdn.net/uploads/201407/26/1406382861_8606.jpg"76         "http://img.my.csdn.net/uploads/201407/26/1406382861_8949.jpg"77         "http://img.my.csdn.net/uploads/201407/26/1406382841_9821.jpg"78         "http://img.my.csdn.net/uploads/201407/26/1406382840_6603.jpg"79         "http://img.my.csdn.net/uploads/201407/26/1406382840_2405.jpg"80         "http://img.my.csdn.net/uploads/201407/26/1406382840_6354.jpg"81         "http://img.my.csdn.net/uploads/201407/26/1406382839_5779.jpg"82         "http://img.my.csdn.net/uploads/201407/26/1406382810_7578.jpg"83         "http://img.my.csdn.net/uploads/201407/26/1406382810_2436.jpg"84         "http://img.my.csdn.net/uploads/201407/26/1406382809_3883.jpg"85         "http://img.my.csdn.net/uploads/201407/26/1406382809_6269.jpg"86         "http://img.my.csdn.net/uploads/201407/26/1406382808_4179.jpg"87         "http://img.my.csdn.net/uploads/201407/26/1406382790_8326.jpg"88         "http://img.my.csdn.net/uploads/201407/26/1406382789_7174.jpg"89         "http://img.my.csdn.net/uploads/201407/26/1406382789_5170.jpg"90         "http://img.my.csdn.net/uploads/201407/26/1406382789_4118.jpg"91         "http://img.my.csdn.net/uploads/201407/26/1406382788_9532.jpg"92         "http://img.my.csdn.net/uploads/201407/26/1406382767_3184.jpg"93         "http://img.my.csdn.net/uploads/201407/26/1406382767_4772.jpg"94         "http://img.my.csdn.net/uploads/201407/26/1406382766_4924.jpg"95         "http://img.my.csdn.net/uploads/201407/26/1406382766_5762.jpg"96         "http://img.my.csdn.net/uploads/201407/26/1406382765_7341.jpg"
97     };
98 }@H_198_301@

View Code

DiskLruCache(磁盘缓存类):

  1 /*
  2  * Copyright (C) 2011 The Android Open Source Project
  3   4  * Licensed under the Apache License,Version 2.0 (the "License");
  5  * you may not use this file except in compliance with the License.
  6  * You may obtain a copy of the License at
  7   8  *      http://www.apache.org/licenses/LICENSE-2.0
  9  10  * Unless required by applicable law or agreed to in writing,software
 11  * distributed under the License is distributed on an "AS IS" BASIS,1)"> 12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,either express or implied.
 13  * See the License for the specific language governing permissions and
 14  * limitations under the License.
 15   16 
 17  18 
 19 import java.io.BufferedInputStream;
 20  java.io.BufferedWriter;
 21  java.io.Closeable;
 22  java.io.EOFException;
 23  java.io.File;
 24  java.io.FileInputStream;
 25  java.io.FileNotFoundException;
 26  java.io.FileOutputStream;
 27  java.io.FileWriter;
 28  java.io.FilterOutputStream;
 29  java.io.IOException;
 30  java.io.InputStream;
 31  java.io.InputStreamReader;
 32  java.io.OutputStream;
 33  java.io.OutputStreamWriter;
 34  java.io.Reader;
 35  java.io.StringWriter;
 36  java.io.Writer;
 37  java.lang.reflect.Array;
 38  java.nio.charset.Charset;
 39  java.util.ArrayList;
 40  java.util.Arrays;
 41  java.util.Iterator;
 42  java.util.LinkedHashMap;
 43  java.util.Map;
 44  java.util.concurrent.Callable;
 45  java.util.concurrent.ExecutorService;
 46  java.util.concurrent.LinkedBlockingQueue;
 47  java.util.concurrent.ThreadPoolExecutor;
 48  java.util.concurrent.TimeUnit;
 49 
 50  51  ******************************************************************************
 52  * Taken from the JB source code,can be found in:
 53  * libcore/luni/src/main/java/libcore/io/DiskLruCache.java
 54  * or direct link:
 55  * https://android.googlesource.com/platform/libcore/+/android-4.1.1_r1/luni/src/main/java/libcore/io/DiskLruCache.java
 56  57  58  * A cache that uses a bounded amount of space on a filesystem. Each cache
 59  * entry has a string key and a fixed number of values. Values are byte
 60  * sequences,accessible as streams or files. Each value must be between {@code
 61  * 0} and {@code Integer.MAX_VALUE} bytes in length.
 62  63  * <p>The cache stores its data in a directory on the filesystem. This
 64  * directory must be exclusive to the cache; the cache may delete or overwrite
 65  * files from its directory. It is an error for multiple processes to use the
 66  * same cache directory at the same time.
 67  68  * <p>This cache limits the number of bytes that it will store on the
 69  * filesystem. When the number of stored bytes exceeds the limit,the cache will
 70  * remove entries in the background until the limit is satisfied. The limit is
 71  * not strict: the cache may temporarily exceed it while waiting for files to be
 72  * deleted. The limit does not include filesystem overhead or the cache
 73  * journal so space-sensitive applications should set a conservative limit.
 74  75  * <p>Clients call {@link #edit} to create or update the values of an entry. An
 76  * entry may have only one editor at one time; if a value is not available to be
 77  * edited then { #edit} will return null.
 78  * <ul>
 79  *     <li>When an entry is being <strong>created</strong> it is necessary to
 80  *         supply a full set of values; the empty value should be used as a
 81  *         placeholder if necessary.
 82  *     <li>When an entry is being <strong>edited</strong>,it is not necessary
 83  *         to supply data for every value; values default to their prevIoUs
 84  *         value.
 85  * </ul>
 86  * Every { #edit} call must be matched by a call to { Editor#commit}
 87  * or { Editor#abort}. Committing is atomic: a read observes the full set
 88  * of values as they were before or after the commit,but never a mix of values.
 89  90  #get} to read a snapshot of an entry. The read will
 91  * observe the value at the time that { #get} was called. Updates and
 92  * removals after the call do not impact ongoing reads.
 93  94  * <p>This class is tolerant of some I/O errors. If files are missing from the
 95  * filesystem,the corresponding entries will be dropped from the cache. If
 96  * an error occurs while writing a cache value,the edit will fail silently.
 97  * Callers should handle other problems by catching { IOException} and
 98  * responding appropriately.
 99  100 class DiskLruCache implements Closeable {
101     static final String JOURNAL_FILE = "journal";
102     final String JOURNAL_FILE_TMP = "journal.tmp"103     final String MAGIC = "libcore.io.DiskLruCache"104     final String VERSION_1 = "1"105     long ANY_SEQUENCE_NUMBER = -1106     private final String CLEAN = "CLEAN"107     final String DIRTY = "DIRTY"108     final String REMOVE = "REMOVE"109     final String READ = "READ"110 
111     final Charset UTF_8 = Charset.forName("UTF-8");
112     int IO_BUFFER_SIZE = 8 * 1024113 
114     115      * This cache uses a journal file named "journal". A typical journal file
116      * looks like this:
117      *     libcore.io.DiskLruCache
118      *     1
119      *     100
120      *     2
121      *
122      *     CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
123      *     DIRTY 335c4c6028171cfddfbaae1a9c313c52
124      *     CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
125      *     REMOVE 335c4c6028171cfddfbaae1a9c313c52
126      *     DIRTY 1ab96a171faeeee38496d8b330771a7a
127      *     CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
128      *     READ 335c4c6028171cfddfbaae1a9c313c52
129      *     READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
130 131      * The first five lines of the journal form its header. They are the
132      * constant string "libcore.io.DiskLruCache",the disk cache's version,1)">133      * the application's version,the value count,and a blank line.
134 135      * Each of the subsequent lines in the file is a record of the state of a
136      * cache entry. Each line contains space-separated values: a state,a key,1)">137      * and optional state-specific values.
138      *   o DIRTY lines track that an entry is actively being created or updated.
139      *     Every successful DIRTY action should be followed by a CLEAN or REMOVE
140      *     action. DIRTY lines without a matching CLEAN or REMOVE indicate that
141      *     temporary files may need to be deleted.
142      *   o CLEAN lines track a cache entry that has been successfully published
143      *     and may be read. A publish line is followed by the lengths of each of
144      *     its values.
145      *   o READ lines track accesses for LRU.
146      *   o REMOVE lines track entries that have been deleted.
147 148      * The journal file is appended to as cache operations occur. The journal may
149      * occasionally be compacted by dropping redundant lines. A temporary file named
150      * "journal.tmp" will be used during compaction; that file should be deleted if
151      * it exists when the cache is opened.
152      153 
154     final File directory;
155      File journalFile;
156      File journalFileTmp;
157     int appVersion;
158     long maxSize;
159      valueCount;
160     long size = 0161     private Writer journalWriter;
162     final LinkedHashMap<String,Entry> lruEntries
163             = new LinkedHashMap<String,Entry>(0,0.75f,true164      redundantOpCount;
165 
166     167      * To differentiate between old and current snapshots,each entry is given
168      * a sequence number each time an edit is committed. A snapshot is stale if
169      * its sequence number is not equal to its entry's sequence number.
170      171     long nextSequenceNumber = 0172 
173     /* From java.util.Arrays 174     @SuppressWarnings("unchecked")
175     static <T> T[] copyOfRange(T[] original,1)">int start,1)"> end) {
176         int originalLength = original.length; // For exception priority compatibility.
177         if (start >178             throw  IllegalArgumentException();
179         }
180         if (start < 0 || start > originalLength) {
181              ArrayIndexOutOfBoundsException();
182 183         int resultLength = end - start;
184         int copyLength = Math.min(resultLength,originalLength - start);
185         final T[] result = (T[]) Array
186                 .newInstance(original.getClass().getComponentType(),resultLength);
187         System.arraycopy(original,start,result,0188         return result;
189     }
190 
191     192      * Returns the remainder of 'reader' as a string,closing it when done.
193      194     static String readFully(Reader reader) throws IOException {
195         try {
196             StringWriter writer =  StringWriter();
197             char[] buffer = new char[1024];
198              count;
199             while ((count = reader.read(buffer)) != -1) {
200                 writer.write(buffer,count);
201             }
202              writer.toString();
203         } finally204             reader.close();
205 206 207 
208     209      * Returns the ASCII characters up to but not including the next "rn",or
210      * "n".
211 212      * @throws java.io.EOFException if the stream is exhausted before the next newline
213      *     character.
214      215     static String readAsciiLine(InputStream in) 216          TODO: support UTF-8 here instead
217 
218         StringBuilder result = new StringBuilder(80219         while (220             int c = in.read();
221             if (c == -1222                  EOFException();
223             } else if (c == 'n'224                 break225 226 
227             result.append((char) c);
228 229         int length = result.length();
230         if (length > 0 && result.charAt(length - 1) == 'r'231             result.setLength(length - 1232 233          result.toString();
234 235 
236     237      * Closes 'closeable',ignoring any checked exceptions. Does nothing if 'closeable' is null.
238      239     void closeQuietly(Closeable closeable) {
240         if (closeable != null241             242                 closeable.close();
243             } catch (RuntimeException rethrown) {
244                 throw rethrown;
245             }  (Exception ignored) {
246 247 248 249 
250     251      * Recursively delete everything in { dir}.
252      253      TODO: this should specify paths as Strings rather than as Files
254     void deleteContents(File dir) 255         File[] files = dir.listFiles();
256         if (files == 257             new IllegalArgumentException("not a directory: " + dir);
258 259         for (File file : files) {
260             if (file.isDirectory()) {
261                 deleteContents(file);
262 263             if (!file.delete()) {
264                 new IOException("Failed to delete file: " + file);
265 266 267 268 
269     /** This cache uses a single background thread to evict entries. 270     final ExecutorService executorService = new ThreadPoolExecutor(0,1271             60L,TimeUnit.SECONDS,1)">new LinkedBlockingQueue<Runnable>());
272     final Callable<Void> cleanupCallable = new Callable<Void>() {
273         @Override public Void call()  Exception {
274             synchronized (DiskLruCache.this275                 if (journalWriter == 276                     return null;  closed
277                 }
278                 trimToSize();
279                  (journalRebuildrequired()) {
280                     rebuildJournal();
281                     redundantOpCount = 0282 283 284             285 286 287 
288     private DiskLruCache(File directory,1)">int appVersion,1)">int valueCount,1)"> maxSize) {
289         this.directory = directory;
290         this.appVersion =291         this.journalFile =  File(directory,JOURNAL_FILE);
292         this.journalFileTmp = 293         this.valueCount =294         this.maxSize =295 296 
297     298      * Opens the cache in { directory},creating a cache if none exists
299      * there.
300 301 @param directory a writable directory
302  appVersion
303  valueCount the number of values per cache entry. Must be positive.
304  maxSize the maximum number of bytes this cache should use to store
305  java.io.IOException if reading or writing the cache directory fails
306      307     static DiskLruCache open(File directory,1)"> maxSize)
308             309         if (maxSize <= 0310             new IllegalArgumentException("maxSize <= 0"311 312         if (valueCount <= 0313             new IllegalArgumentException("valueCount <= 0"314 315 
316          prefer to pick up where we left off
317         DiskLruCache cache =  DiskLruCache(directory,appVersion,valueCount,maxSize);
318          (cache.journalFile.exists()) {
319             320                 cache.readJournal();
321                 cache.processJournal();
322                 cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile,1)">),1)">323                         IO_BUFFER_SIZE);
324                  cache;
325             }  (IOException journalIsCorrupt) {
326                 System.logW("DiskLruCache " + directory + " is corrupt: "
327                         + journalIsCorrupt.getMessage() + ",removing");
328                 cache.delete();
329 330 331 
332          create a new empty cache
333         directory.mkdirs();
334         cache = 335         cache.rebuildJournal();
336         337 338 
339     void readJournal() 340         InputStream in = new BufferedInputStream( FileInputStream(journalFile),IO_BUFFER_SIZE);
341         342             String magic = readAsciiLine(in);
343             String version =344             String appVersionString =345             String valueCountString =346             String blank =347             MAGIC.equals(magic)
348                     || !VERSION_1.equals(version)
349                     || !Integer.toString(appVersion).equals(appVersionString)
350                     || !Integer.toString(valueCount).equals(valueCountString)
351                     || !"".equals(blank)) {
352                 new IOException("unexpected journal header: ["
353                         + magic + "," + version + "," + valueCountString + "," + blank + "]"354 355 
356             357                 358                     readJournalLine(readAsciiLine(in));
359                 }  (EOFException endOfJournal) {
360                     361 362 363         } 364             closeQuietly(in);
365 366 367 
368     void readJournalLine(String line) 369         String[] parts = line.split(" "370         if (parts.length < 2371             new IOException("unexpected journal line: " + line);
372 373 
374         String key = parts[1375         if (parts[0].equals(REMOVE) && parts.length == 2376             lruEntries.remove(key);
377             378 379 
380         Entry entry = lruEntries.get(key);
381         if (entry == 382             entry =  Entry(key);
383             lruEntries.put(key,entry);
384 385 
386         if (parts[0].equals(CLEAN) && parts.length == 2 + valueCount) {
387             entry.readable = 388             entry.currentEditor = 389             entry.setLengths(copyOfRange(parts,2390         } if (parts[0].equals(DIRTY) && parts.length == 2391             entry.currentEditor =  Editor(entry);
392         } if (parts[0].equals(READ) && parts.length == 2393              this work was already done by calling lruEntries.get()
394         } else395             396 397 398 
399     400      * Computes the initial size and collects garbage as a part of opening the
401      * cache. Dirty entries are assumed to be inconsistent and will be deleted.
402      403     void processJournal() 404         deleteIfExists(journalFileTmp);
405         for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) {
406             Entry entry = i.next();
407             if (entry.currentEditor == 408                 for (int t = 0; t < valueCount; t++409                     size += entry.lengths[t];
410 411             } 412                 entry.currentEditor = 413                 414                     deleteIfExists(entry.getCleanFile(t));
415                     deleteIfExists(entry.getDirtyFile(t));
416 417                 i.remove();
418 419 420 421 
422     423      * Creates a new journal that omits redundant information. This replaces the
424      * current journal if it exists.
425      426     synchronized void rebuildJournal() 427         if (journalWriter != 428             journalWriter.close();
429 430 
431         Writer writer =  FileWriter(journalFileTmp),1)">432         writer.write(MAGIC);
433         writer.write("n"434         writer.write(VERSION_1);
435         writer.write("n"436         writer.write(Integer.toString(appVersion));
437         writer.write("n"438         writer.write(Integer.toString(valueCount));
439         writer.write("n"440         writer.write("n"441 
442          (Entry entry : lruEntries.values()) {
443             if (entry.currentEditor != 444                 writer.write(DIRTY + ' ' + entry.key + 'n'445             } 446                 writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + 'n'447 448 449 
450         writer.close();
451         journalFileTmp.renameTo(journalFile);
452         journalWriter = new FileWriter(journalFile,1)">453 454 
455     void deleteIfExists(File file) 456         try {
457             Libcore.os.remove(file.getPath());
458         } catch (ErrnoException errnoException) {
459             if (errnoException.errno != OsConstants.ENOENT) {
460                 throw errnoException.rethrowAsIOException();
461 462         }
463         if (file.exists() && !464              IOException();
465 466 467 
468     469      * Returns a snapshot of the entry named { key},or null if it doesn't
470      * exist is not currently readable. If a value is returned,it is moved to
471      * the head of the LRU queue.
472      473     synchronized Snapshot get(String key) 474         checkNotClosed();
475         validateKey(key);
476         Entry entry =477         478             479 480 
481         entry.readable) {
482             483 484 
485         486          * Open all streams eagerly to guarantee that we see a single published
487          * snapshot. If we opened streams lazily then the streams could come
488          * from different edits.
489          490         InputStream[] ins =  InputStream[valueCount];
491         492             int i = 0; i < valueCount; i++493                 ins[i] =  FileInputStream(entry.getCleanFile(i));
494 495         }  (FileNotFoundException e) {
496              a file must have been deleted manually!
497             498 499 
500         redundantOpCount++501         journalWriter.append(READ + ' ' + key + 'n'502         503             executorService.submit(cleanupCallable);
504 505 
506          Snapshot(key,entry.sequenceNumber,ins);
507 508 
509     510      * Returns an editor for the entry named {511      * edit is in progress.
512      513     public Editor edit(String key) 514          edit(key,ANY_SEQUENCE_NUMBER);
515 516 
517     synchronized Editor edit(String key,1)">long expectedSequenceNumber) 518 519 520         Entry entry =521         if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER
522                 && (entry == null || entry.sequenceNumber != expectedSequenceNumber)) {
523              snapshot is stale
524 525         526             entry = 527 528         } 529              another edit is in progress
530 531 
532         Editor editor = 533         entry.currentEditor = editor;
534 
535          flush the journal before creating files to prevent file leaks
536         journalWriter.write(DIRTY + ' ' + key + 'n'537         journalWriter.flush();
538         539 540 
541     542      * Returns the directory where this cache stores its data.
543      544     public File getDirectory() {
545         546 547 
548     549      * Returns the maximum number of bytes that this cache should use to store
550      * its data.
551      552      maxSize() {
553         554 555 
556     557      * Returns the number of bytes currently being used to store the values in
558      * this cache. This may be greater than the max size if a background
559      * deletion is pending.
560      561      size() {
562          size;
563 564 
565     void completeEdit(Editor editor,1)">boolean success) 566         Entry entry = editor.entry;
567         if (entry.currentEditor != editor) {
568              IllegalStateException();
569 570 
571          if this edit is creating the entry for the first time,every index must have a value
572         if (success && !573             574                 entry.getDirtyFile(i).exists()) {
575                     editor.abort();
576                     new IllegalStateException("edit didn't create file " + i);
577 578 579 580 
581         582             File dirty = entry.getDirtyFile(i);
583              (success) {
584                  (dirty.exists()) {
585                     File clean = entry.getCleanFile(i);
586                     dirty.renameTo(clean);
587                     long oldLength = entry.lengths[i];
588                     long newLength = clean.length();
589                     entry.lengths[i] = newLength;
590                     size = size - oldLength +591 592             } 593                 deleteIfExists(dirty);
594 595 596 
597         redundantOpCount++598         entry.currentEditor = 599         if (entry.readable | success) {
600             entry.readable = 601             journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + 'n'602             603                 entry.sequenceNumber = nextSequenceNumber++604 605         } 606             lruEntries.remove(entry.key);
607             journalWriter.write(REMOVE + ' ' + entry.key + 'n'608 609 
610         if (size > maxSize || journalRebuildrequired()) {
611 612 613 614 
615     616      * We only rebuild the journal when it will halve the size of the journal
617      * and eliminate at least 2000 ops.
618      619     boolean journalRebuildrequired() {
620         int REDUNDANT_OP_COMPACT_THRESHOLD = 2000621         return redundantOpCount >= REDUNDANT_OP_COMPACT_THRESHOLD
622                 && redundantOpCount >= lruEntries.size();
623 624 
625     626      * Drops the entry for { key} if it exists and can be removed. Entries
627      * actively being edited cannot be removed.
628 629 @return true if an entry was removed.
630      631     boolean remove(String key) 632 633 634         Entry entry =635         null || entry.currentEditor != 636             false637 638 
639         640             File file =641             642                 new IOException("Failed to delete " +643 644             size -=645             entry.lengths[i] = 0646 647 
648         redundantOpCount++649         journalWriter.append(REMOVE + ' ' + key + 'n'650         lruEntries.remove(key);
651 
652         653 654 655 
656         657 658 
659     660      * Returns true if this cache has been closed.
661      662      isClosed() {
663         return journalWriter == 664 665 
666      checkNotClosed() {
667         668             new IllegalStateException("cache is closed"669 670 671 
672     673      * Force buffered operations to the filesystem.
674      675     void flush() 676 677         trimToSize();
678 679 680 
681     682      * Closes this cache. Stored values will remain on the filesystem.
683      684     void close() 685         686             return;  already closed
687 688         for (Entry entry : new ArrayList<Entry>(lruEntries.values())) {
689             690                 entry.currentEditor.abort();
691 692 693 694         journalWriter.close();
695         journalWriter = 696 697 
698     void trimToSize() 699         while (size >700             Map.Entry<String,Entry> toEvict = lruEntries.eldest();
701             final Map.Entry<String,Entry> toEvict = lruEntries.entrySet().iterator().next();
702             remove(toEvict.getKey());
703 704 705 
706     707      * Closes the cache and deletes all of its stored values. This will delete
708      * all files in the cache directory including files that weren't created by
709      * the cache.
710      711     void delete() 712         close();
713         deleteContents(directory);
714 715 
716      validateKey(String key) {
717         if (key.contains(" ") || key.contains("n") || key.contains("r")) {
718              IllegalArgumentException(
719                     "keys must not contain spaces or newlines: "" + key + """720 721 722 
723     static String inputStreamToString(InputStream in) 724         return readFully( InputStreamReader(in,UTF_8));
725 726 
727     728      * A snapshot of the values for an entry.
729      730     class Snapshot 731          String key;
732          sequenceNumber;
733          InputStream[] ins;
734 
735         private Snapshot(String key,1)"> sequenceNumber,InputStream[] ins) {
736             this.key = key;
737             this.sequenceNumber =738             this.ins = ins;
739 740 
741         742          * Returns an editor for this snapshot's entry,or null if either the
743          * entry has changed since this snapshot was created or if another edit
744          * is in progress.
745          746         public Editor edit() 747             return DiskLruCache..edit(key,sequenceNumber);
748 749 
750         751          * Returns the unbuffered stream with the value for { index}.
752          753         public InputStream getInputStream( index) {
754              ins[index];
755 756 
757         758          * Returns the string value for {759          760         public String getString(int index) 761              inputStreamToString(getInputStream(index));
762 763 
764         @Override  close() {
765              (InputStream in : ins) {
766                 closeQuietly(in);
767 768 769 770 
771     772      * Edits the values for an entry.
773      774      Editor {
775          Entry entry;
776          hasErrors;
777 
778          Editor(Entry entry) {
779             this.entry = entry;
780 781 
782         783          * Returns an unbuffered input stream to read the last committed value,1)">784          * or null if no value has been committed.
785          786         public InputStream newInputStream(787             788                 789                     790 791                 792                     793 794                  FileInputStream(entry.getCleanFile(index));
795 796 797 
798         799          * Returns the last committed value as a string,or null if no value
800          * has been committed.
801          802         803             InputStream in = newInputStream(index);
804             return in != null ? inputStreamToString(in) : 805 806 
807         808          * Returns a new unbuffered output stream to write the value at
809          * { index}. If the underlying output stream encounters errors
810          * when writing to the filesystem,this edit will be aborted when
811  #commit} is called. The returned output stream does not throw
812          * IOExceptions.
813          814         public OutputStream newOutputStream(815             816                 817                     818 819                 new FaultHidingOutputStream( FileOutputStream(entry.getDirtyFile(index)));
820 821 822 
823         824          * Sets the value at { index} to { value}.
825          826         void set(int index,String value) 827             Writer writer = 828             829                 writer =  OutputStreamWriter(newOutputStream(index),UTF_8);
830                 writer.write(value);
831             } 832                 closeQuietly(writer);
833 834 835 
836         837          * Commits this edit so it is visible to readers.  This releases the
838          * edit lock so another edit may be started on the same key.
839          840         void commit() 841              (hasErrors) {
842                 completeEdit(this,1)">843                 remove(entry.key);  the prevIoUs entry is stale
844             } 845                 completeEdit(846 847 848 
849         850          * Aborts this edit. This releases the edit lock so another edit may be
851          * started on the same key.
852          853         void abort() 854             completeEdit(855 856 
857         class FaultHidingOutputStream extends FilterOutputStream {
858              FaultHidingOutputStream(OutputStream out) {
859                 super(out);
860 861 
862             @Override void write( oneByte) {
863                 864                     out.write(oneByte);
865                 }  (IOException e) {
866                     hasErrors = 867 868 869 
870             @Override byte[] buffer,1)">int offset,1)"> length) {
871                 872                     out.write(buffer,offset,length);
873                 } 874                     hasErrors = 875 876 877 
878             @Override 879                 880                     out.close();
881                 } 882                     hasErrors = 883 884 885 
886             @Override  flush() {
887                 888                     out.flush();
889                 } 890                     hasErrors = 891 892 893 894 895 
896      Entry {
897         898 
899          Lengths of this entry's files. 900         [] lengths;
901 
902          True if this entry has ever been published 903          readable;
904 
905          The ongoing edit or null if this entry is not being edited. 906          Editor currentEditor;
907 
908          The sequence number of the most recently committed edit to this entry. 909         910 
911          Entry(String key) {
912             913             this.lengths = [valueCount];
914 915 
916         public String getLengths() 917             StringBuilder result =  StringBuilder();
918              size : lengths) {
919                 result.append(' ').append(size);
920 921             922 923 
924         925          * Set lengths using decimal numbers like "10123".
926          927         void setLengths(String[] strings) 928             if (strings.length !=929                  invalidLengths(strings);
930 931 
932             933                 int i = 0; i < strings.length; i++934                     lengths[i] = Long.parseLong(strings[i]);
935 936             }  (NumberFormatException e) {
937                 938 939 940 
941         private IOException invalidLengths(String[] strings) 942              Arrays.toString(strings));
943 944 
945         public File getCleanFile( i) {
946             new File(directory,key + "." +947 948 
949         public File getDirtyFile(950             951 952 953 }@H_198_301@

View Code

MD5Utils(MD5转换工具类):

 3  java.math.BigInteger;
 java.security.MessageDigest;
 java.security.NoSuchAlgorithmException;
 6 
 MD5Utils {
 8          * 使用md5的算法进行加密
10      static String md5(String plainText) {
12         byte[] secretBytes = 13         14             secretBytes = MessageDigest.getInstance("md5").digest(
15                     plainText.getBytes());
16         }  (NoSuchAlgorithmException e) {
17             new RuntimeException("没有md5这个算法!"18 19         String md5code = new BigInteger(1,secretBytes).toString(16); 16进制数字
20          如果生成数字未满32位,需要前面补0
21         int i = 0; i < 32 - md5code.length(); i++22             md5code = "0" + md5code;
23 24         25 26 
27 }@H_198_301@

View Code

上面三个类直接引入项目就行了,接下来说说核心实现代码了。

PhotoWallAdapter(GridView适配器类):

说下思路:由于我们的图片源是单纯的字符串(网址),这里给GridView适配的Adpter采用ArrayAdapter,当然如果你想用BaseAdatper也是可以的,思路不变。

1、继承ArrayAdatper,在构造函数传入必要参数后,需要进行2个操作:1、对内存缓存类的初始化  2、对磁盘缓存的初始化

2、然后在getView方法中来设置图片源,首先先设置成一张默认的图片,然后根据图片的URL去缓存中找是否有相关联的资源,如果没找到在磁盘缓存中找,如果还是没找到再去网络上下载,然后保存在磁盘缓存里,在以上的任一环节(磁盘,网络)里,只要我们找到了相对的图片资源,我们就把它添加到内存缓存中,以便下一次的引用。为了避免异步下载图片造成的图片错位现象,我们在每一个ImageView里设置了一个标识符Tag,Tag为图片的唯一标志:URL地址。

3、由于磁盘缓存属于I/O操作,网络属于下载操作都是属于耗时性的工作,这里我们开启了一个内部类Async异步类去完成,把所有的耗时操作都安排在doInBackground里执行(这里选择Async而不选择直接new Thread的原因是,Async运用了线程池的概念,会比单开子线程会更省资源,而且所有的任务会按照队列的顺序去执行)

?由于这里的图片都是比较小的,在实际开发中,大家可以对利用BitmapFactory的Options类对图片进行压缩再展示。

  2 
 java.io.BufferedOutputStream;
 java.net.HttpURLConnection;
 java.net.URL;
 java.util.HashSet;
 java.util.Set;
 12 
 android.content.Context;
 android.content.pm.PackageInfo;
 15  android.content.pm.PackageManager.NameNotFoundException;
 16  android.graphics.Bitmap;
 android.graphics.BitmapFactory;
 18  android.os.AsyncTask;
 android.os.Environment;
 android.support.v4.util.LruCache;
 android.util.Log;
 android.view.LayoutInflater;
 android.view.View;
 android.view.ViewGroup;
 android.widget.ArrayAdapter;
 android.widget.GridView;
 android.widget.ImageView;
 28 
 com.example.photoswall.DiskLruCache.Snapshot;
 30 
class PhotoWallAdapter extends ArrayAdapter<String> 32 
 33      声明LruCache缓存对象
 34     private LruCache<String,Bitmap> lruCache;
 35      声明DiskLruCache硬盘缓存对象
 36      DiskLruCache diskLruCache;
 37      任务队列
 38     private Set<LoadImageAsync> tasks;
 39      声明GridView对象
 40      GridView gridView;
 41 
 42     public PhotoWallAdapter(Context context,1)"> textViewResourceId,String[] objects,GridView gridView) {
 43         (context,textViewResourceId,objects);
 44         this.gridView = gridView;
 45         tasks = new HashSet<LoadImageAsync>();
 46                  * 初始化内存缓存LruCache
 48           49          获取应用程序最大可占内存值
 50         int maxMemory = () Runtime.getRuntime().maxMemory();
 51          设置最大内存的八分之一作为缓存大小
 52         int lruMemory = maxMemory / 8 53         lruCache = new LruCache<String,1)">(lruMemory) {
            @Override
 55             protected  sizeOf(String key,Bitmap bitmap) {
 56                  返回Bitmap对象所占大小,单位:kb
 57                  bitmap.getByteCount();
 59 
        };
 61                  * 初始化硬盘缓存DiskLruCahce
 63           64          获取硬盘缓存路径,参数二为所在缓存路径的文件夹名称
 65         File directory = getDiskCacheDir(getContext(),"bitmap" 66         directory.exists()) {
 67              若文件夹不存在,建立文件夹
            directory.mkdirs();
 70         int appVersion = getAppVersion(getContext());
 71          72              参数1:缓存文件路径,参数2:系统版本号,参数3:一个缓存路径对于几个文件,参数4:缓存空间大小:字节
 73             diskLruCache = DiskLruCache.open(directory,1,1024 * 1024 * 10 74         }             e.printStackTrace();
 77 
 79 
 80      context
 uniqueName
@return 
     *         当SD卡存在或者SD卡不可被移除的时候,就调用getExternalCacheDir()方法来获取缓存路径,否则就调用getCacheDir
     *         ()方法来获取缓存路径。 前者获取到的就是 /sdcard/Android/data/<application
     *         package>/cache 这个路径 而后者获取到的是 datadata/application package>/cache
     *         这个路径。
 88       89      File getDiskCacheDir(Context context,String uniqueName) {
        String cachePath;
 91         if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) || !Environment.isExternalStorageRemovable()) {
 92             cachePath = context.getExternalCacheDir().getPath();
 93         }  94             cachePath = context.getCacheDir().getPath();
 96         new File(cachePath + File.separator + uniqueName);
 98 
 99     101  获取系统版本号
102       getAppVersion(Context context) {
104         105             PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(),1)">106              info.versionCode;
107         }  (NameNotFoundException e) {
108 109 110         return 1111 112 
113     @Override
public View getView( position,View convertView,ViewGroup parent) {
115          获取图片资源URL地址
116         String path = getItem(position);
117         View view = 118         if (convertView == 119             view = LayoutInflater.from(getContext()).inflate(R.layout.gridview_item,1)">120         } 121             view = convertView;
123          获取控件实例
124         ImageView imageView = (ImageView) view.findViewById(R.id.iv_photo);
125          设置一个唯一标识符,避免异步加载图片时错位
        imageView.setTag(path);
127          设置默认图片
        imageView.setImageResource(R.drawable.ic_launcher);
129          根据图片URL到缓存中去找图片资源并设置
        setImageFromLruCache(path,imageView);
131          view;
133 
134          * 根据图片URL地址获取缓存中图片,若不存在去磁盘缓存中查找=》网络下载
     * 
 path
 imageView
139      140      setImageFromLruCache(String path,ImageView imageView) {
141         Bitmap bitmap = lruCache.get(path);
142         if (bitmap != 143              缓存存在,取出设置图片
144             Log.i("PhotoWallAdapter","在内存缓存中找到"            imageView.setImageBitmap(bitmap);
146         } 147              缓存不存在,先找硬盘缓存,还不存在,就去网络下载(开启异步任务)
148             LoadImageAsync loadImageAsync =  LoadImageAsync();
            loadImageAsync.execute(path);
150              添加任务到任务队列
            tasks.add(loadImageAsync);
152 153 154 
156      * 取消队列中准备下载和正在下载的任务
157       cancelTask() {
159          (LoadImageAsync task : tasks) {
160             task.cancel(161 162 163 
165      * 同步内存操作到journal文件
166      167      flushCache() {
168         if (diskLruCache != 169             170                 diskLruCache.flush();
171             } 172                 e.printStackTrace();
173 174 175 
176 177 
178     class LoadImageAsync extends AsyncTask<String,Void,1)">179          图片资源URL
180         String path = 181 
        @Override
protected Bitmap doInBackground(String... params) {
184              图片下载地址
185             this.path = params[0186             Snapshot snapshot = 187             OutputStream outputStream = 188             Bitmap bitmap = 189             String pathMd5 = MD5Utils.md5(path);
190              根据图片url(md5)查找图片资源是否存在于硬盘缓存
191             192                 snapshot = diskLruCache.get(pathMd5);
193                 if (snapshot == 194                      在磁盘缓存中没有找到图片资源
195                      获取一个DiskLruCache写入对象
196                     DiskLruCache.Editor editor = diskLruCache.edit(pathMd5);
197                     if (editor != 198                         outputStream = editor.newOutputStream(0199                          开启异步网络任务获取图片,并存入磁盘缓存
200                          (downloadUrlToStream(path,outputStream)) {
201                              下载成功
202                             Log.i("PhotoWallAdapter","下载文件成功"203                             editor.commit();
204                         }                             editor.abort();
                        }
207                     }
208 209                  图片写入磁盘缓存后,再一次的查找磁盘缓存
210                 snapshot =211                 if (snapshot != 212                      若查找到,获取图片,并把图片资源写入内存缓存
213                     bitmap = BitmapFactory.decodeStream(snapshot.getInputStream(0));
214                     Log.i("PhotoWallAdapter","在磁盘缓存中找到"215 216                 217                      将Bitmap对象添加到内存缓存当中
218                     lruCache.put(path,bitmap);
219 220                  bitmap;
221             } 222 if (outputStream != 225                     226                         outputStream.close();
227                     }                         e.printStackTrace();
229 230 231 232             233 234 
235 236          onPostExecute(Bitmap bitmap) {
237             .onPostExecute(bitmap);
238              根据Tag获取控件对象并设置图片
239             ImageView imageView = (ImageView) gridView.findViewWithTag(path);
240             if (imageView != null && bitmap != 241                  加载图片
                imageView.setImageBitmap(bitmap);
243 244             tasks.remove(245 
247 
248         249          * 根据图片URL地址下载图片,成功返回true,失败false
250          * 
         *  urlString
252  outputStream
253 @return
254          255          downloadUrlToStream(String urlString,OutputStream outputStream) {
256             HttpURLConnection urlConnection = 257             BufferedOutputStream out = 258             BufferedInputStream in = 259             260                 final URL url =  URL(urlString);
261                 urlConnection = (HttpURLConnection) url.openConnection();
262                 in = new BufferedInputStream(urlConnection.getInputStream(),8 * 1024263                 out = new BufferedOutputStream(outputStream,1)"> b;
265                 while ((b = in.read()) != -1                    out.write(b);
268                 269             } catch ( IOException e) {
270 271             } 272                 if (urlConnection != 273                     urlConnection.disconnect();
274 if (out !=                         out.close();
279                     if (in !=                         in.close();
281 282                 }                     e.printStackTrace();
284 286             287 288 
289 290 
291 }@H_198_301@

?MainActivity类:

相比之下这个类就简单许多了,这里需要注意的两点:

1、在onPause方法里进行flush操作(更新磁盘缓存操作日志,磁盘缓存之所以能够被读取取决于日志文件)

2、在Activity销毁之前取消所有正在下载和准备下载的任务。

 android.app.Activity;
 android.os.Bundle;
class MainActivity  Activity {
 8 
 9      GridView gv_photo;
10      PhotoWallAdapter adapter;
11 
12 13      onCreate(Bundle savedInstanceState) {
14         .onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
16         gv_photo = (GridView) findViewById(R.id.gv_photo);
17         adapter = new PhotoWallAdapter(MainActivity.        gv_photo.setAdapter(adapter);
19 
20 21 
22 23      onPause() {
.onPause();
        adapter.flushCache();
26 27 
28 29      onDestroy() {
30         .onDestroy();
31         adapter.cancelTask();
32 33 
34 }@H_198_301@

?

剩下的一些细节,代码注释的很详细,还有不清楚的可以看看上文提到附带的2篇文章或者评论给我留言。

?

作者:Balla_兔子
出处:http://www.cnblogs.com/lichenwei/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文链接。
正在看本人博客的这位童鞋,我看你气度不凡,谈吐间隐隐有王者之气,日后必有一番作为!旁边有“推荐”二字,你就顺手把它点了吧,相得准,我分文不收;相不准,你也好回来找我!

(编辑:北几岛)

【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容!

    推荐文章
      热点阅读