zoukankan      html  css  js  c++  java
  • 安卓开发笔记——关于图片的三级缓存策略(内存LruCache+磁盘DiskLruCache+网络Volley)

      在开发安卓应用中避免不了要使用到网络图片,获取网络图片很简单,但是需要付出一定的代价——流量。对于少数的图片而言问题不大,但如果手机应用中包含大量的图片,这势必会耗费用户的一定流量,如果我们不加以处理,每次打开应用都去网络获取图片,那么用户可就不乐意了,这里的处理就是指今天要讲的缓存策略(缓存层分为三层:内存层,磁盘层,网络层)。

      关于缓存层的工作,当我们第一次打开应用获取图片时,先到网络去下载图片,然后依次存入内存缓存,磁盘缓存,当我们再一次需要用到刚才下载的这张图片时,就不需要再重复的到网络上去下载,直接可以从内存缓存和磁盘缓存中找,由于内存缓存速度较快,我们优先到内存缓存中寻找该图片,如果找到则运用,如果没有找到(内存缓存大小有限),那么我们再到磁盘缓存中去找。只要我们合理的去协调这三层缓存运用,便可以提升应用性能和用户体验。

    1、内存层:(手机内存)

    内存缓存相对于磁盘缓存而言,速度要来的快很多,但缺点容量较小且会被系统回收,这里的实现我用到了LruCache。

    LruCache这个类是Android3.1版本中提供的,如果你是在更早的Android版本中开发,则需要导入android-support-v4的jar包。

    磁盘层:(SD卡)

    相比内存缓存而言速度要来得慢很多,但容量很大,这里的实现我用到了DiskLruCache类。

    DiskLruCache是非Google官方编写,但获得官方认证的硬盘缓存类,该类没有限定在Android内,所以理论上java应用也可以使用DiskLreCache来缓存。

    这是DiskLruCache类的下载地址:http://pan.baidu.com/s/1hq0D53m 

    网络层:(移动网络,无线网络)

    这个就没什么解释的了,就是我们上网用的流量。这里的网络访问实现我用到了开源框架Volley。

    开源框架Volley是2013年Google I/O大会发布的,Volley是Android平台上的网络通信库,能使网络通信更快,更简单,更健壮。它的设计目标就是非常适合去进行数据量不大,但通信频繁的网络操作,而对于大数据量的网络操作,比如说下载文件等,Volley的表现就会非常糟糕。

    这是Volley的下载地址:http://pan.baidu.com/s/1hq1t2yo

    先来看下效果图:

    正常网络下:                                        断开网络,飞行模式下:

             

    Log日志打印:

    来看下代码实现:

    1、由于应用中很多地方需要用到上下文对象,这里我自定义了一个全局的Application,用来提供上下文对象

     1 package com.lcw.rabbit.image.utils;
     2 
     3 import android.app.Application;
     4 /**
     5  * Application类,提供全局上下文对象
     6  * @author Rabbit_Lee
     7  *
     8  */
     9 public class MyApplication extends Application {
    10 
    11     public static String TAG;
    12     public static MyApplication myApplication;
    13 
    14     public static MyApplication newInstance() {
    15         return myApplication;
    16     }
    17 
    18     @Override
    19     public void onCreate() {
    20         super.onCreate();
    21         TAG = this.getClass().getSimpleName();
    22         myApplication = this;
    23 
    24     }
    25 }

    2、Volley请求队列处理类,用来管理Rquest请求对象操作

     1 package com.lcw.rabbit.image;
     2 
     3 import com.android.volley.Request;
     4 import com.android.volley.RequestQueue;
     5 import com.android.volley.toolbox.Volley;
     6 import com.lcw.rabbit.image.utils.MyApplication;
     7 
     8 /**
     9  * 请求队列处理类
    10  * 获取RequestQueue对象
    11  */
    12 public class VolleyRequestQueueManager {
    13     // 获取请求队列类
    14     public static RequestQueue mRequestQueue = Volley.newRequestQueue(MyApplication.newInstance());
    15 
    16     //添加任务进任务队列
    17     public static void addRequest(Request<?> request, Object tag) {
    18         if (tag != null) {
    19             request.setTag(tag);
    20         }
    21         mRequestQueue.add(request);
    22     }
    23     
    24     //取消任务
    25     public static void cancelRequest(Object tag){
    26         mRequestQueue.cancelAll(tag);
    27     }
    28     
    29     
    30 
    31 }

    3、这里附上2个工具类(生成MD5序列帮助类,DiskLruCache磁盘缓存类)

     1 package com.lcw.rabbit.image.utils;
     2 
     3 import java.math.BigInteger;
     4 import java.security.MessageDigest;
     5 import java.security.NoSuchAlgorithmException;
     6 
     7 public class MD5Utils {
     8     /**
     9      * 使用md5的算法进行加密
    10      */
    11     public static String md5(String plainText) {
    12         byte[] secretBytes = null;
    13         try {
    14             secretBytes = MessageDigest.getInstance("md5").digest(
    15                     plainText.getBytes());
    16         } catch (NoSuchAlgorithmException e) {
    17             throw new RuntimeException("没有md5这个算法!");
    18         }
    19         String md5code = new BigInteger(1, secretBytes).toString(16);// 16进制数字
    20         // 如果生成数字未满32位,需要前面补0
    21         for (int i = 0; i < 32 - md5code.length(); i++) {
    22             md5code = "0" + md5code;
    23         }
    24         return md5code;
    25     }
    26 
    27 }
    MD5转换类
      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,
     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 package com.lcw.rabbit.image.utils;
     18 
     19 import java.io.BufferedInputStream;
     20 import java.io.BufferedWriter;
     21 import java.io.Closeable;
     22 import java.io.EOFException;
     23 import java.io.File;
     24 import java.io.FileInputStream;
     25 import java.io.FileNotFoundException;
     26 import java.io.FileOutputStream;
     27 import java.io.FileWriter;
     28 import java.io.FilterOutputStream;
     29 import java.io.IOException;
     30 import java.io.InputStream;
     31 import java.io.InputStreamReader;
     32 import java.io.OutputStream;
     33 import java.io.OutputStreamWriter;
     34 import java.io.Reader;
     35 import java.io.StringWriter;
     36 import java.io.Writer;
     37 import java.lang.reflect.Array;
     38 import java.nio.charset.Charset;
     39 import java.util.ArrayList;
     40 import java.util.Arrays;
     41 import java.util.Iterator;
     42 import java.util.LinkedHashMap;
     43 import java.util.Map;
     44 import java.util.concurrent.Callable;
     45 import java.util.concurrent.ExecutorService;
     46 import java.util.concurrent.LinkedBlockingQueue;
     47 import java.util.concurrent.ThreadPoolExecutor;
     48 import 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 {@link #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 {@link #edit} call must be matched by a call to {@link Editor#commit}
     87  * or {@link 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  * <p>Clients call {@link #get} to read a snapshot of an entry. The read will
     91  * observe the value at the time that {@link #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 {@code IOException} and
     98  * responding appropriately.
     99  */
    100 public final class DiskLruCache implements Closeable {
    101     static final String JOURNAL_FILE = "journal";
    102     static final String JOURNAL_FILE_TMP = "journal.tmp";
    103     static final String MAGIC = "libcore.io.DiskLruCache";
    104     static final String VERSION_1 = "1";
    105     static final long ANY_SEQUENCE_NUMBER = -1;
    106     private static final String CLEAN = "CLEAN";
    107     private static final String DIRTY = "DIRTY";
    108     private static final String REMOVE = "REMOVE";
    109     private static final String READ = "READ";
    110 
    111     private static final Charset UTF_8 = Charset.forName("UTF-8");
    112     private static final int IO_BUFFER_SIZE = 8 * 1024;
    113 
    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,
    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,
    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     private final File directory;
    155     private final File journalFile;
    156     private final File journalFileTmp;
    157     private final int appVersion;
    158     private final long maxSize;
    159     private final int valueCount;
    160     private long size = 0;
    161     private Writer journalWriter;
    162     private final LinkedHashMap<String, Entry> lruEntries
    163             = new LinkedHashMap<String, Entry>(0, 0.75f, true);
    164     private int 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     private long nextSequenceNumber = 0;
    172 
    173     /* From java.util.Arrays */
    174     @SuppressWarnings("unchecked")
    175     private static <T> T[] copyOfRange(T[] original, int start, int end) {
    176         final int originalLength = original.length; // For exception priority compatibility.
    177         if (start > end) {
    178             throw new IllegalArgumentException();
    179         }
    180         if (start < 0 || start > originalLength) {
    181             throw new ArrayIndexOutOfBoundsException();
    182         }
    183         final int resultLength = end - start;
    184         final 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, 0, copyLength);
    188         return result;
    189     }
    190 
    191     /**
    192      * Returns the remainder of 'reader' as a string, closing it when done.
    193      */
    194     public static String readFully(Reader reader) throws IOException {
    195         try {
    196             StringWriter writer = new StringWriter();
    197             char[] buffer = new char[1024];
    198             int count;
    199             while ((count = reader.read(buffer)) != -1) {
    200                 writer.write(buffer, 0, count);
    201             }
    202             return writer.toString();
    203         } finally {
    204             reader.close();
    205         }
    206     }
    207 
    208     /**
    209      * Returns the ASCII characters up to but not including the next "
    ", or
    210      * "
    ".
    211      *
    212      * @throws java.io.EOFException if the stream is exhausted before the next newline
    213      *     character.
    214      */
    215     public static String readAsciiLine(InputStream in) throws IOException {
    216         // TODO: support UTF-8 here instead
    217 
    218         StringBuilder result = new StringBuilder(80);
    219         while (true) {
    220             int c = in.read();
    221             if (c == -1) {
    222                 throw new EOFException();
    223             } else if (c == '
    ') {
    224                 break;
    225             }
    226 
    227             result.append((char) c);
    228         }
    229         int length = result.length();
    230         if (length > 0 && result.charAt(length - 1) == '
    ') {
    231             result.setLength(length - 1);
    232         }
    233         return result.toString();
    234     }
    235 
    236     /**
    237      * Closes 'closeable', ignoring any checked exceptions. Does nothing if 'closeable' is null.
    238      */
    239     public static void closeQuietly(Closeable closeable) {
    240         if (closeable != null) {
    241             try {
    242                 closeable.close();
    243             } catch (RuntimeException rethrown) {
    244                 throw rethrown;
    245             } catch (Exception ignored) {
    246             }
    247         }
    248     }
    249 
    250     /**
    251      * Recursively delete everything in {@code dir}.
    252      */
    253     // TODO: this should specify paths as Strings rather than as Files
    254     public static void deleteContents(File dir) throws IOException {
    255         File[] files = dir.listFiles();
    256         if (files == null) {
    257             throw 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                 throw new IOException("failed to delete file: " + file);
    265             }
    266         }
    267     }
    268 
    269     /** This cache uses a single background thread to evict entries. */
    270     private final ExecutorService executorService = new ThreadPoolExecutor(0, 1,
    271             60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
    272     private final Callable<Void> cleanupCallable = new Callable<Void>() {
    273         @Override public Void call() throws Exception {
    274             synchronized (DiskLruCache.this) {
    275                 if (journalWriter == null) {
    276                     return null; // closed
    277                 }
    278                 trimToSize();
    279                 if (journalRebuildRequired()) {
    280                     rebuildJournal();
    281                     redundantOpCount = 0;
    282                 }
    283             }
    284             return null;
    285         }
    286     };
    287 
    288     private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) {
    289         this.directory = directory;
    290         this.appVersion = appVersion;
    291         this.journalFile = new File(directory, JOURNAL_FILE);
    292         this.journalFileTmp = new File(directory, JOURNAL_FILE_TMP);
    293         this.valueCount = valueCount;
    294         this.maxSize = maxSize;
    295     }
    296 
    297     /**
    298      * Opens the cache in {@code directory}, creating a cache if none exists
    299      * there.
    300      *
    301      * @param directory a writable directory
    302      * @param appVersion
    303      * @param valueCount the number of values per cache entry. Must be positive.
    304      * @param maxSize the maximum number of bytes this cache should use to store
    305      * @throws java.io.IOException if reading or writing the cache directory fails
    306      */
    307     public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
    308             throws IOException {
    309         if (maxSize <= 0) {
    310             throw new IllegalArgumentException("maxSize <= 0");
    311         }
    312         if (valueCount <= 0) {
    313             throw new IllegalArgumentException("valueCount <= 0");
    314         }
    315 
    316         // prefer to pick up where we left off
    317         DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
    318         if (cache.journalFile.exists()) {
    319             try {
    320                 cache.readJournal();
    321                 cache.processJournal();
    322                 cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true),
    323                         IO_BUFFER_SIZE);
    324                 return cache;
    325             } catch (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 = new DiskLruCache(directory, appVersion, valueCount, maxSize);
    335         cache.rebuildJournal();
    336         return cache;
    337     }
    338 
    339     private void readJournal() throws IOException {
    340         InputStream in = new BufferedInputStream(new FileInputStream(journalFile), IO_BUFFER_SIZE);
    341         try {
    342             String magic = readAsciiLine(in);
    343             String version = readAsciiLine(in);
    344             String appVersionString = readAsciiLine(in);
    345             String valueCountString = readAsciiLine(in);
    346             String blank = readAsciiLine(in);
    347             if (!MAGIC.equals(magic)
    348                     || !VERSION_1.equals(version)
    349                     || !Integer.toString(appVersion).equals(appVersionString)
    350                     || !Integer.toString(valueCount).equals(valueCountString)
    351                     || !"".equals(blank)) {
    352                 throw new IOException("unexpected journal header: ["
    353                         + magic + ", " + version + ", " + valueCountString + ", " + blank + "]");
    354             }
    355 
    356             while (true) {
    357                 try {
    358                     readJournalLine(readAsciiLine(in));
    359                 } catch (EOFException endOfJournal) {
    360                     break;
    361                 }
    362             }
    363         } finally {
    364             closeQuietly(in);
    365         }
    366     }
    367 
    368     private void readJournalLine(String line) throws IOException {
    369         String[] parts = line.split(" ");
    370         if (parts.length < 2) {
    371             throw new IOException("unexpected journal line: " + line);
    372         }
    373 
    374         String key = parts[1];
    375         if (parts[0].equals(REMOVE) && parts.length == 2) {
    376             lruEntries.remove(key);
    377             return;
    378         }
    379 
    380         Entry entry = lruEntries.get(key);
    381         if (entry == null) {
    382             entry = new Entry(key);
    383             lruEntries.put(key, entry);
    384         }
    385 
    386         if (parts[0].equals(CLEAN) && parts.length == 2 + valueCount) {
    387             entry.readable = true;
    388             entry.currentEditor = null;
    389             entry.setLengths(copyOfRange(parts, 2, parts.length));
    390         } else if (parts[0].equals(DIRTY) && parts.length == 2) {
    391             entry.currentEditor = new Editor(entry);
    392         } else if (parts[0].equals(READ) && parts.length == 2) {
    393             // this work was already done by calling lruEntries.get()
    394         } else {
    395             throw new IOException("unexpected journal line: " + line);
    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     private void processJournal() throws IOException {
    404         deleteIfExists(journalFileTmp);
    405         for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) {
    406             Entry entry = i.next();
    407             if (entry.currentEditor == null) {
    408                 for (int t = 0; t < valueCount; t++) {
    409                     size += entry.lengths[t];
    410                 }
    411             } else {
    412                 entry.currentEditor = null;
    413                 for (int t = 0; t < valueCount; t++) {
    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     private synchronized void rebuildJournal() throws IOException {
    427         if (journalWriter != null) {
    428             journalWriter.close();
    429         }
    430 
    431         Writer writer = new BufferedWriter(new FileWriter(journalFileTmp), IO_BUFFER_SIZE);
    432         writer.write(MAGIC);
    433         writer.write("
    ");
    434         writer.write(VERSION_1);
    435         writer.write("
    ");
    436         writer.write(Integer.toString(appVersion));
    437         writer.write("
    ");
    438         writer.write(Integer.toString(valueCount));
    439         writer.write("
    ");
    440         writer.write("
    ");
    441 
    442         for (Entry entry : lruEntries.values()) {
    443             if (entry.currentEditor != null) {
    444                 writer.write(DIRTY + ' ' + entry.key + '
    ');
    445             } else {
    446                 writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '
    ');
    447             }
    448         }
    449 
    450         writer.close();
    451         journalFileTmp.renameTo(journalFile);
    452         journalWriter = new BufferedWriter(new FileWriter(journalFile, true), IO_BUFFER_SIZE);
    453     }
    454 
    455     private static void deleteIfExists(File file) throws IOException {
    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() && !file.delete()) {
    464             throw new IOException();
    465         }
    466     }
    467 
    468     /**
    469      * Returns a snapshot of the entry named {@code 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     public synchronized Snapshot get(String key) throws IOException {
    474         checkNotClosed();
    475         validateKey(key);
    476         Entry entry = lruEntries.get(key);
    477         if (entry == null) {
    478             return null;
    479         }
    480 
    481         if (!entry.readable) {
    482             return null;
    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 = new InputStream[valueCount];
    491         try {
    492             for (int i = 0; i < valueCount; i++) {
    493                 ins[i] = new FileInputStream(entry.getCleanFile(i));
    494             }
    495         } catch (FileNotFoundException e) {
    496             // a file must have been deleted manually!
    497             return null;
    498         }
    499 
    500         redundantOpCount++;
    501         journalWriter.append(READ + ' ' + key + '
    ');
    502         if (journalRebuildRequired()) {
    503             executorService.submit(cleanupCallable);
    504         }
    505 
    506         return new Snapshot(key, entry.sequenceNumber, ins);
    507     }
    508 
    509     /**
    510      * Returns an editor for the entry named {@code key}, or null if another
    511      * edit is in progress.
    512      */
    513     public Editor edit(String key) throws IOException {
    514         return edit(key, ANY_SEQUENCE_NUMBER);
    515     }
    516 
    517     private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
    518         checkNotClosed();
    519         validateKey(key);
    520         Entry entry = lruEntries.get(key);
    521         if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER
    522                 && (entry == null || entry.sequenceNumber != expectedSequenceNumber)) {
    523             return null; // snapshot is stale
    524         }
    525         if (entry == null) {
    526             entry = new Entry(key);
    527             lruEntries.put(key, entry);
    528         } else if (entry.currentEditor != null) {
    529             return null; // another edit is in progress
    530         }
    531 
    532         Editor editor = new Editor(entry);
    533         entry.currentEditor = editor;
    534 
    535         // flush the journal before creating files to prevent file leaks
    536         journalWriter.write(DIRTY + ' ' + key + '
    ');
    537         journalWriter.flush();
    538         return editor;
    539     }
    540 
    541     /**
    542      * Returns the directory where this cache stores its data.
    543      */
    544     public File getDirectory() {
    545         return directory;
    546     }
    547 
    548     /**
    549      * Returns the maximum number of bytes that this cache should use to store
    550      * its data.
    551      */
    552     public long maxSize() {
    553         return maxSize;
    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     public synchronized long size() {
    562         return size;
    563     }
    564 
    565     private synchronized void completeEdit(Editor editor, boolean success) throws IOException {
    566         Entry entry = editor.entry;
    567         if (entry.currentEditor != editor) {
    568             throw new IllegalStateException();
    569         }
    570 
    571         // if this edit is creating the entry for the first time, every index must have a value
    572         if (success && !entry.readable) {
    573             for (int i = 0; i < valueCount; i++) {
    574                 if (!entry.getDirtyFile(i).exists()) {
    575                     editor.abort();
    576                     throw new IllegalStateException("edit didn't create file " + i);
    577                 }
    578             }
    579         }
    580 
    581         for (int i = 0; i < valueCount; i++) {
    582             File dirty = entry.getDirtyFile(i);
    583             if (success) {
    584                 if (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 + newLength;
    591                 }
    592             } else {
    593                 deleteIfExists(dirty);
    594             }
    595         }
    596 
    597         redundantOpCount++;
    598         entry.currentEditor = null;
    599         if (entry.readable | success) {
    600             entry.readable = true;
    601             journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '
    ');
    602             if (success) {
    603                 entry.sequenceNumber = nextSequenceNumber++;
    604             }
    605         } else {
    606             lruEntries.remove(entry.key);
    607             journalWriter.write(REMOVE + ' ' + entry.key + '
    ');
    608         }
    609 
    610         if (size > maxSize || journalRebuildRequired()) {
    611             executorService.submit(cleanupCallable);
    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     private boolean journalRebuildRequired() {
    620         final int REDUNDANT_OP_COMPACT_THRESHOLD = 2000;
    621         return redundantOpCount >= REDUNDANT_OP_COMPACT_THRESHOLD
    622                 && redundantOpCount >= lruEntries.size();
    623     }
    624 
    625     /**
    626      * Drops the entry for {@code 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     public synchronized boolean remove(String key) throws IOException {
    632         checkNotClosed();
    633         validateKey(key);
    634         Entry entry = lruEntries.get(key);
    635         if (entry == null || entry.currentEditor != null) {
    636             return false;
    637         }
    638 
    639         for (int i = 0; i < valueCount; i++) {
    640             File file = entry.getCleanFile(i);
    641             if (!file.delete()) {
    642                 throw new IOException("failed to delete " + file);
    643             }
    644             size -= entry.lengths[i];
    645             entry.lengths[i] = 0;
    646         }
    647 
    648         redundantOpCount++;
    649         journalWriter.append(REMOVE + ' ' + key + '
    ');
    650         lruEntries.remove(key);
    651 
    652         if (journalRebuildRequired()) {
    653             executorService.submit(cleanupCallable);
    654         }
    655 
    656         return true;
    657     }
    658 
    659     /**
    660      * Returns true if this cache has been closed.
    661      */
    662     public boolean isClosed() {
    663         return journalWriter == null;
    664     }
    665 
    666     private void checkNotClosed() {
    667         if (journalWriter == null) {
    668             throw new IllegalStateException("cache is closed");
    669         }
    670     }
    671 
    672     /**
    673      * Force buffered operations to the filesystem.
    674      */
    675     public synchronized void flush() throws IOException {
    676         checkNotClosed();
    677         trimToSize();
    678         journalWriter.flush();
    679     }
    680 
    681     /**
    682      * Closes this cache. Stored values will remain on the filesystem.
    683      */
    684     public synchronized void close() throws IOException {
    685         if (journalWriter == null) {
    686             return; // already closed
    687         }
    688         for (Entry entry : new ArrayList<Entry>(lruEntries.values())) {
    689             if (entry.currentEditor != null) {
    690                 entry.currentEditor.abort();
    691             }
    692         }
    693         trimToSize();
    694         journalWriter.close();
    695         journalWriter = null;
    696     }
    697 
    698     private void trimToSize() throws IOException {
    699         while (size > maxSize) {
    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     public void delete() throws IOException {
    712         close();
    713         deleteContents(directory);
    714     }
    715 
    716     private void validateKey(String key) {
    717         if (key.contains(" ") || key.contains("
    ") || key.contains("
    ")) {
    718             throw new IllegalArgumentException(
    719                     "keys must not contain spaces or newlines: "" + key + """);
    720         }
    721     }
    722 
    723     private static String inputStreamToString(InputStream in) throws IOException {
    724         return readFully(new InputStreamReader(in, UTF_8));
    725     }
    726 
    727     /**
    728      * A snapshot of the values for an entry.
    729      */
    730     public final class Snapshot implements Closeable {
    731         private final String key;
    732         private final long sequenceNumber;
    733         private final InputStream[] ins;
    734 
    735         private Snapshot(String key, long sequenceNumber, InputStream[] ins) {
    736             this.key = key;
    737             this.sequenceNumber = 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() throws IOException {
    747             return DiskLruCache.this.edit(key, sequenceNumber);
    748         }
    749 
    750         /**
    751          * Returns the unbuffered stream with the value for {@code index}.
    752          */
    753         public InputStream getInputStream(int index) {
    754             return ins[index];
    755         }
    756 
    757         /**
    758          * Returns the string value for {@code index}.
    759          */
    760         public String getString(int index) throws IOException {
    761             return inputStreamToString(getInputStream(index));
    762         }
    763 
    764         @Override public void close() {
    765             for (InputStream in : ins) {
    766                 closeQuietly(in);
    767             }
    768         }
    769     }
    770 
    771     /**
    772      * Edits the values for an entry.
    773      */
    774     public final class Editor {
    775         private final Entry entry;
    776         private boolean hasErrors;
    777 
    778         private Editor(Entry entry) {
    779             this.entry = entry;
    780         }
    781 
    782         /**
    783          * Returns an unbuffered input stream to read the last committed value,
    784          * or null if no value has been committed.
    785          */
    786         public InputStream newInputStream(int index) throws IOException {
    787             synchronized (DiskLruCache.this) {
    788                 if (entry.currentEditor != this) {
    789                     throw new IllegalStateException();
    790                 }
    791                 if (!entry.readable) {
    792                     return null;
    793                 }
    794                 return new 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         public String getString(int index) throws IOException {
    803             InputStream in = newInputStream(index);
    804             return in != null ? inputStreamToString(in) : null;
    805         }
    806 
    807         /**
    808          * Returns a new unbuffered output stream to write the value at
    809          * {@code index}. If the underlying output stream encounters errors
    810          * when writing to the filesystem, this edit will be aborted when
    811          * {@link #commit} is called. The returned output stream does not throw
    812          * IOExceptions.
    813          */
    814         public OutputStream newOutputStream(int index) throws IOException {
    815             synchronized (DiskLruCache.this) {
    816                 if (entry.currentEditor != this) {
    817                     throw new IllegalStateException();
    818                 }
    819                 return new FaultHidingOutputStream(new FileOutputStream(entry.getDirtyFile(index)));
    820             }
    821         }
    822 
    823         /**
    824          * Sets the value at {@code index} to {@code value}.
    825          */
    826         public void set(int index, String value) throws IOException {
    827             Writer writer = null;
    828             try {
    829                 writer = new OutputStreamWriter(newOutputStream(index), UTF_8);
    830                 writer.write(value);
    831             } finally {
    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         public void commit() throws IOException {
    841             if (hasErrors) {
    842                 completeEdit(this, false);
    843                 remove(entry.key); // the previous entry is stale
    844             } else {
    845                 completeEdit(this, true);
    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         public void abort() throws IOException {
    854             completeEdit(this, false);
    855         }
    856 
    857         private class FaultHidingOutputStream extends FilterOutputStream {
    858             private FaultHidingOutputStream(OutputStream out) {
    859                 super(out);
    860             }
    861 
    862             @Override public void write(int oneByte) {
    863                 try {
    864                     out.write(oneByte);
    865                 } catch (IOException e) {
    866                     hasErrors = true;
    867                 }
    868             }
    869 
    870             @Override public void write(byte[] buffer, int offset, int length) {
    871                 try {
    872                     out.write(buffer, offset, length);
    873                 } catch (IOException e) {
    874                     hasErrors = true;
    875                 }
    876             }
    877 
    878             @Override public void close() {
    879                 try {
    880                     out.close();
    881                 } catch (IOException e) {
    882                     hasErrors = true;
    883                 }
    884             }
    885 
    886             @Override public void flush() {
    887                 try {
    888                     out.flush();
    889                 } catch (IOException e) {
    890                     hasErrors = true;
    891                 }
    892             }
    893         }
    894     }
    895 
    896     private final class Entry {
    897         private final String key;
    898 
    899         /** Lengths of this entry's files. */
    900         private final long[] lengths;
    901 
    902         /** True if this entry has ever been published */
    903         private boolean readable;
    904 
    905         /** The ongoing edit or null if this entry is not being edited. */
    906         private Editor currentEditor;
    907 
    908         /** The sequence number of the most recently committed edit to this entry. */
    909         private long sequenceNumber;
    910 
    911         private Entry(String key) {
    912             this.key = key;
    913             this.lengths = new long[valueCount];
    914         }
    915 
    916         public String getLengths() throws IOException {
    917             StringBuilder result = new StringBuilder();
    918             for (long size : lengths) {
    919                 result.append(' ').append(size);
    920             }
    921             return result.toString();
    922         }
    923 
    924         /**
    925          * Set lengths using decimal numbers like "10123".
    926          */
    927         private void setLengths(String[] strings) throws IOException {
    928             if (strings.length != valueCount) {
    929                 throw invalidLengths(strings);
    930             }
    931 
    932             try {
    933                 for (int i = 0; i < strings.length; i++) {
    934                     lengths[i] = Long.parseLong(strings[i]);
    935                 }
    936             } catch (NumberFormatException e) {
    937                 throw invalidLengths(strings);
    938             }
    939         }
    940 
    941         private IOException invalidLengths(String[] strings) throws IOException {
    942             throw new IOException("unexpected journal line: " + Arrays.toString(strings));
    943         }
    944 
    945         public File getCleanFile(int i) {
    946             return new File(directory, key + "." + i);
    947         }
    948 
    949         public File getDirtyFile(int i) {
    950             return new File(directory, key + "." + i + ".tmp");
    951         }
    952     }
    953 }
    DiskLruCache磁盘缓存类

    4、图片缓存类,包含(LruCache内存缓存,DiskLruCache磁盘缓存)

      1 package com.lcw.rabbit.image.utils;
      2 
      3 import java.io.File;
      4 import java.io.IOException;
      5 import java.io.OutputStream;
      6 
      7 import android.content.Context;
      8 import android.content.pm.PackageInfo;
      9 import android.content.pm.PackageManager.NameNotFoundException;
     10 import android.graphics.Bitmap;
     11 import android.graphics.Bitmap.CompressFormat;
     12 import android.graphics.BitmapFactory;
     13 import android.os.Environment;
     14 import android.support.v4.util.LruCache;
     15 import android.util.Log;
     16 
     17 import com.android.volley.toolbox.ImageLoader.ImageCache;
     18 import com.lcw.rabbit.image.utils.DiskLruCache.Snapshot;
     19 
     20 /**
     21  * 图片缓存帮助类
     22  * 
     23  * 包含内存缓存LruCache和磁盘缓存DiskLruCache
     24  * 
     25  * @author Rabbit_Lee
     26  * 
     27  */
     28 public class ImageCacheUtil implements ImageCache {
     29     
     30     private String TAG=ImageCacheUtil.this.getClass().getSimpleName();
     31 
     32     //缓存类
     33     private static LruCache<String, Bitmap> mLruCache;
     34     private static DiskLruCache mDiskLruCache;
     35 
     36     //磁盘缓存大小
     37     private static final int DISKMAXSIZE = 10 * 1024 * 1024;
     38 
     39     public ImageCacheUtil() {
     40         // 获取应用可占内存的1/8作为缓存
     41         int maxSize = (int) (Runtime.getRuntime().maxMemory() / 8);
     42         // 实例化LruCaceh对象
     43         mLruCache = new LruCache<String, Bitmap>(maxSize) {
     44             @Override
     45             protected int sizeOf(String key, Bitmap bitmap) {
     46                 return bitmap.getRowBytes() * bitmap.getHeight();
     47             }
     48         };
     49         try {
     50             // 获取DiskLruCahce对象
     51             mDiskLruCache = DiskLruCache.open(getDiskCacheDir(MyApplication.newInstance(), "Rabbit"), getAppVersion(MyApplication.newInstance()), 1, DISKMAXSIZE);
     52         } catch (IOException e) {
     53             e.printStackTrace();
     54         }
     55     }
     56 
     57     /**
     58      * 从缓存(内存缓存,磁盘缓存)中获取Bitmap
     59      */
     60     @Override
     61     public Bitmap getBitmap(String url) {
     62         if (mLruCache.get(url) != null) {
     63             // 从LruCache缓存中取
     64             Log.i(TAG,"从LruCahce获取");
     65             return mLruCache.get(url);
     66         } else {
     67             String key = MD5Utils.md5(url);
     68             try {
     69                 if (mDiskLruCache.get(key) != null) {
     70                     // 从DiskLruCahce取
     71                     Snapshot snapshot = mDiskLruCache.get(key);
     72                     Bitmap bitmap = null;
     73                     if (snapshot != null) {
     74                         bitmap = BitmapFactory.decodeStream(snapshot.getInputStream(0));
     75                         // 存入LruCache缓存
     76                         mLruCache.put(url, bitmap);
     77                         Log.i(TAG,"从DiskLruCahce获取");
     78                     }
     79                     return bitmap;
     80                 }
     81             } catch (IOException e) {
     82                 e.printStackTrace();
     83             }
     84         }
     85         return null;
     86     }
     87 
     88     /**
     89      * 存入缓存(内存缓存,磁盘缓存)
     90      */
     91     @Override
     92     public void putBitmap(String url, Bitmap bitmap) {
     93         // 存入LruCache缓存
     94         mLruCache.put(url, bitmap);
     95         // 判断是否存在DiskLruCache缓存,若没有存入
     96         String key = MD5Utils.md5(url);
     97         try {
     98             if (mDiskLruCache.get(key) == null) {
     99                 DiskLruCache.Editor editor = mDiskLruCache.edit(key);
    100                 if (editor != null) {
    101                     OutputStream outputStream = editor.newOutputStream(0);
    102                     if (bitmap.compress(CompressFormat.JPEG, 100, outputStream)) {
    103                         editor.commit();
    104                     } else {
    105                         editor.abort();
    106                     }
    107                 }
    108                 mDiskLruCache.flush();
    109             }
    110         } catch (IOException e) {
    111             e.printStackTrace();
    112         }
    113 
    114     }
    115 
    116     /**
    117      * 该方法会判断当前sd卡是否存在,然后选择缓存地址
    118      * 
    119      * @param context
    120      * @param uniqueName
    121      * @return
    122      */
    123     public static File getDiskCacheDir(Context context, String uniqueName) {
    124         String cachePath;
    125         if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) || !Environment.isExternalStorageRemovable()) {
    126             cachePath = context.getExternalCacheDir().getPath();
    127         } else {
    128             cachePath = context.getCacheDir().getPath();
    129         }
    130         return new File(cachePath + File.separator + uniqueName);
    131     }
    132 
    133     /**
    134      * 获取应用版本号
    135      * 
    136      * @param context
    137      * @return
    138      */
    139     public int getAppVersion(Context context) {
    140         try {
    141             PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
    142             return info.versionCode;
    143         } catch (NameNotFoundException e) {
    144             e.printStackTrace();
    145         }
    146         return 1;
    147     }
    148 
    149 }

    5、图片缓存管理类

    这里的图片加载运用到了Volley自带的ImageLoader,在它的构造方法中需要一个ImageCache对象,在上面的图片缓存类已实现了该接口。

    这里向外部提供了一个loadImage的重载方法,一个传入加载图片的宽高,一个默认加载原图,使得外部不再需要关注任何关于缓存的操作。

     1 package com.lcw.rabbit.image;
     2 
     3 import android.graphics.Bitmap;
     4 import android.widget.ImageView;
     5 
     6 import com.android.volley.VolleyError;
     7 import com.android.volley.toolbox.ImageLoader;
     8 import com.android.volley.toolbox.ImageLoader.ImageCache;
     9 import com.android.volley.toolbox.ImageLoader.ImageContainer;
    10 import com.android.volley.toolbox.ImageLoader.ImageListener;
    11 import com.lcw.rabbit.image.utils.ImageCacheUtil;
    12 
    13 /**
    14  * 图片缓存管理类 获取ImageLoader对象
    15  * 
    16  * @author Rabbit_Lee
    17  * 
    18  */
    19 public class ImageCacheManager {
    20 
    21     private static String TAG = ImageCacheManager.class.getSimpleName();
    22 
    23     // 获取图片缓存类对象
    24     private static ImageCache mImageCache = new ImageCacheUtil();
    25     // 获取ImageLoader对象
    26     public static ImageLoader mImageLoader = new ImageLoader(VolleyRequestQueueManager.mRequestQueue, mImageCache);
    27 
    28     /**
    29      * 获取ImageListener
    30      * 
    31      * @param view
    32      * @param defaultImage
    33      * @param errorImage
    34      * @return
    35      */
    36     public static ImageListener getImageListener(final ImageView view, final Bitmap defaultImage, final Bitmap errorImage) {
    37 
    38         return new ImageListener() {
    39 
    40             @Override
    41             public void onErrorResponse(VolleyError error) {
    42                 // 回调失败
    43                 if (errorImage != null) {
    44                     view.setImageBitmap(errorImage);
    45                 }
    46             }
    47 
    48             @Override
    49             public void onResponse(ImageContainer response, boolean isImmediate) {
    50                 // 回调成功
    51                 if (response.getBitmap() != null) {
    52                     view.setImageBitmap(response.getBitmap());
    53                 } else if (defaultImage != null) {
    54                     view.setImageBitmap(defaultImage);
    55                 }
    56             }
    57         };
    58 
    59     }
    60 
    61     /**
    62      * 提供给外部调用方法
    63      * 
    64      * @param url
    65      * @param view
    66      * @param defaultImage
    67      * @param errorImage
    68      */
    69     public static void loadImage(String url, ImageView view, Bitmap defaultImage, Bitmap errorImage) {
    70         mImageLoader.get(url, ImageCacheManager.getImageListener(view, defaultImage, errorImage), 0, 0);
    71     }
    72 
    73     /**
    74      * 提供给外部调用方法
    75      * 
    76      * @param url
    77      * @param view
    78      * @param defaultImage
    79      * @param errorImage
    80      */
    81     public static void loadImage(String url, ImageView view, Bitmap defaultImage, Bitmap errorImage, int maxWidth, int maxHeight) {
    82         mImageLoader.get(url, ImageCacheManager.getImageListener(view, defaultImage, errorImage), maxWidth, maxHeight);
    83     }
    84 }

    6、MainActivity类

     1 package com.lcw.rabbit.image;
     2 
     3 import android.app.Activity;
     4 import android.content.res.Resources;
     5 import android.graphics.Bitmap;
     6 import android.graphics.BitmapFactory;
     7 import android.os.Bundle;
     8 import android.view.View;
     9 import android.view.View.OnClickListener;
    10 import android.widget.Button;
    11 import android.widget.ImageView;
    12 
    13 public class MainActivity extends Activity {
    14 
    15     private Button mButton;
    16     private ImageView mImageView;
    17 
    18     @Override
    19     protected void onCreate(Bundle savedInstanceState) {
    20         super.onCreate(savedInstanceState);
    21         setContentView(R.layout.activity_main);
    22         mButton = (Button) findViewById(R.id.button);
    23         mImageView= (ImageView) findViewById(R.id.image);
    24         
    25 
    26         mButton.setOnClickListener(new OnClickListener() {
    27 
    28             @Override
    29             public void onClick(View v) {
    30                 String url = "http://img.blog.csdn.net/20130702124537953?watermark/2/text/aHR0cDovL2Jsb2cuY3Nkbi5uZXQvdDEyeDM0NTY=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70/gravity/SouthEast";
    31                 ImageCacheManager.loadImage(url, mImageView, getBitmapFromRes(R.drawable.ic_launcher), getBitmapFromRes(R.drawable.ic_launcher));
    32 
    33             }
    34         });
    35     }
    36 
    37     public Bitmap getBitmapFromRes(int resId) {
    38         Resources res = this.getResources();
    39         return BitmapFactory.decodeResource(res, resId);
    40 
    41     }
    42 
    43 }

    到这里代码就结束了,由于主要是讲关于缓存层的运用,关于Volley,LruCache,DiskCache的介绍使用,这里就不再阐述了,网上资料很多,大家查阅下便是。

    有任何疑问或者建议,大家可以在文章评论给我留言,一起交流!

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

  • 相关阅读:
    asp.net pager
    asp.net 2.0
    mul page
    基于 Ajax 的持久对象映射(reship)
    asp.net run
    reship HttpProc
    some questions
    rss feed
    javascript function
    ioc
  • 原文地址:https://www.cnblogs.com/lichenwei/p/4651576.html
Copyright © 2011-2022 走看看