├── .gitignore ├── .idea ├── .name ├── compiler.xml ├── copyright │ └── profiles_settings.xml ├── encodings.xml ├── gradle.xml ├── misc.xml ├── modules.xml ├── runConfigurations.xml └── vcs.xml ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── xc │ │ ├── xccache │ │ ├── Cache.java │ │ ├── CloseUtils.java │ │ ├── DiskCache.java │ │ ├── DiskLruCache.java │ │ ├── MemoryCache.java │ │ └── XCCacheManager.java │ │ └── xccachemanager │ │ └── MainActivity.java │ └── res │ ├── layout │ └── activity_main.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── mipmap-xxxhdpi │ └── ic_launcher.png │ ├── values-w820dp │ └── dimens.xml │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── screenshots └── 01.gif └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | XCCacheManager -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.idea/encodings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 19 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 19 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 46 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # XCCacheManager 2 | XCCacheManager For Android-Android缓存管理工具库 3 | 4 | 使用演示图: 5 | 6 | ![iamge](https://raw.githubusercontent.com/jczmdeveloper/XCCacheManager/master/screenshots/01.gif) 7 | 8 | 使用方法如下: 9 | 10 | public class MainActivity extends Activity implements View.OnClickListener { 11 | private Button mBtnWrite; 12 | private Button mBtnRead; 13 | private EditText mEtText; 14 | private TextView mTvResult; 15 | 16 | private XCCacheManager mCacheManager; 17 | 18 | @Override 19 | protected void onCreate(Bundle savedInstanceState) { 20 | super.onCreate(savedInstanceState); 21 | setContentView(R.layout.activity_main); 22 | mBtnRead = (Button) findViewById(R.id.btn_read); 23 | mBtnWrite = (Button) findViewById(R.id.btn_write); 24 | mEtText = (EditText) findViewById(R.id.et_text); 25 | mTvResult = (TextView) findViewById(R.id.tv_result); 26 | mBtnWrite.setOnClickListener(this); 27 | mBtnRead.setOnClickListener(this); 28 | 29 | mCacheManager = XCCacheManager.getInstance(this); 30 | } 31 | 32 | @Override 33 | public void onClick(View v) { 34 | switch (v.getId()){ 35 | case R.id.btn_read: 36 | mTvResult.setText(mCacheManager.readCache("key_demo")); 37 | break; 38 | case R.id.btn_write: 39 | mCacheManager.writeCache("key_demo",mEtText.getText().toString()); 40 | Toast.makeText(this,"write string to cache",Toast.LENGTH_SHORT).show(); 41 | break; 42 | } 43 | } 44 | } 45 | 46 | 47 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 22 5 | buildToolsVersion "22.0.1" 6 | 7 | defaultConfig { 8 | applicationId "com.xc.xccachemanager" 9 | minSdkVersion 14 10 | targetSdkVersion 22 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(dir: 'libs', include: ['*.jar']) 24 | compile 'com.android.support:support-v4:22.0.0' 25 | } 26 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in D:\Java\AndroidStudio\SDK/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 11 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/java/com/xc/xccache/Cache.java: -------------------------------------------------------------------------------- 1 | package com.xc.xccache; 2 | 3 | /** 4 | * Created by caizhiming on 2015/12/4. 5 | */ 6 | public interface Cache { 7 | String get(final String key); 8 | void put(final String key, final String value); 9 | boolean remove(final String key); 10 | } 11 | -------------------------------------------------------------------------------- /app/src/main/java/com/xc/xccache/CloseUtils.java: -------------------------------------------------------------------------------- 1 | package com.xc.xccache; 2 | 3 | import java.io.Closeable; 4 | import java.io.IOException; 5 | 6 | /** 关闭Closeable对象工具方法 7 | * Created by caizhiming on 2015/12/3. 8 | */ 9 | public final class CloseUtils { 10 | private CloseUtils(){ 11 | 12 | } 13 | /** 14 | * 关闭Closeable对象 15 | */ 16 | public static void closeCloseable(Closeable closeable){ 17 | if(null != closeable){ 18 | try { 19 | closeable.close(); 20 | } catch (IOException e) { 21 | e.printStackTrace(); 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/src/main/java/com/xc/xccache/DiskCache.java: -------------------------------------------------------------------------------- 1 | package com.xc.xccache; 2 | 3 | import android.content.Context; 4 | import android.content.pm.PackageInfo; 5 | import android.content.pm.PackageManager; 6 | import android.graphics.Bitmap; 7 | import android.graphics.BitmapFactory; 8 | import android.os.Environment; 9 | import android.util.Log; 10 | 11 | import java.io.BufferedInputStream; 12 | import java.io.BufferedOutputStream; 13 | import java.io.File; 14 | import java.io.IOException; 15 | import java.io.InputStream; 16 | import java.io.OutputStream; 17 | import java.net.HttpURLConnection; 18 | import java.net.URL; 19 | import java.security.MessageDigest; 20 | import java.security.NoSuchAlgorithmException; 21 | 22 | /** 23 | * Disk磁盘缓存类 24 | * Created by caizhiming on 2015/12/4. 25 | */ 26 | public class DiskCache implements Cache{ 27 | 28 | private DiskLruCache mDiskLruCache = null; 29 | public DiskCache(Context context){ 30 | init(context); 31 | } 32 | /** 33 | * 初始化 DiskLruCache 34 | */ 35 | public void init(Context context){ 36 | try { 37 | File cacheDir = getDiskCacheDir(context, "http_cache"); 38 | if (!cacheDir.exists()) { 39 | cacheDir.mkdirs(); 40 | } 41 | Log.v("czm", "cache file=" + cacheDir.getAbsolutePath()); 42 | mDiskLruCache = DiskLruCache.open(cacheDir, getAppVersion(context), 1, 10 * 1024 * 1024); 43 | } catch (IOException e) { 44 | e.printStackTrace(); 45 | } 46 | } 47 | @Override 48 | public String get(String key) { 49 | String result = null; 50 | try { 51 | DiskLruCache.Snapshot snapShot = mDiskLruCache.get(hashKeyForDisk(key)); 52 | if (snapShot != null) { 53 | result = snapShot.getString(0); 54 | return result; 55 | } 56 | } catch (IOException e) { 57 | e.printStackTrace(); 58 | return null; 59 | } 60 | return result; 61 | } 62 | 63 | @Override 64 | public void put(String key, String value) { 65 | DiskLruCache.Editor editor = null; 66 | try { 67 | editor = mDiskLruCache.edit(hashKeyForDisk(key)); 68 | if (editor != null) { 69 | editor.set(0, value); 70 | editor.commit(); 71 | } 72 | mDiskLruCache.flush(); 73 | } catch (IOException e) { 74 | e.printStackTrace(); 75 | } 76 | } 77 | 78 | @Override 79 | public boolean remove(String key) { 80 | try { 81 | return mDiskLruCache.remove(hashKeyForDisk(key)); 82 | } catch (IOException e) { 83 | e.printStackTrace(); 84 | } 85 | return false; 86 | } 87 | 88 | public Bitmap getImageCache(String key){ 89 | Bitmap bitmap = null; 90 | try { 91 | DiskLruCache.Snapshot snapShot = mDiskLruCache.get(hashKeyForDisk(key)); 92 | if (snapShot != null) { 93 | InputStream is = snapShot.getInputStream(0); 94 | bitmap = BitmapFactory.decodeStream(is); 95 | return bitmap; 96 | } 97 | } catch (IOException e) { 98 | e.printStackTrace(); 99 | } 100 | return bitmap; 101 | } 102 | public void putImageCache(final String key){ 103 | new Thread(new Runnable() { 104 | @Override 105 | public void run() { 106 | try { 107 | DiskLruCache.Editor editor = mDiskLruCache.edit(hashKeyForDisk(key)); 108 | if (editor != null) { 109 | OutputStream outputStream = editor.newOutputStream(0); 110 | if (downloadUrlToStream(key, outputStream)) { 111 | editor.commit(); 112 | } else { 113 | editor.abort(); 114 | } 115 | } 116 | mDiskLruCache.flush(); 117 | } catch (IOException e) { 118 | e.printStackTrace(); 119 | } 120 | } 121 | }).start(); 122 | } 123 | private boolean downloadUrlToStream(String urlString, OutputStream outputStream) { 124 | HttpURLConnection urlConnection = null; 125 | BufferedOutputStream out = null; 126 | BufferedInputStream in = null; 127 | try { 128 | final URL url = new URL(urlString); 129 | urlConnection = (HttpURLConnection) url.openConnection(); 130 | in = new BufferedInputStream(urlConnection.getInputStream(), 8 * 1024); 131 | out = new BufferedOutputStream(outputStream, 8 * 1024); 132 | int b; 133 | while ((b = in.read()) != -1) { 134 | out.write(b); 135 | } 136 | return true; 137 | } catch (final IOException e) { 138 | e.printStackTrace(); 139 | } finally { 140 | if (urlConnection != null) { 141 | urlConnection.disconnect(); 142 | } 143 | CloseUtils.closeCloseable(out); 144 | CloseUtils.closeCloseable(in); 145 | } 146 | return false; 147 | } 148 | 149 | 150 | public String hashKeyForDisk(String key) { 151 | String cacheKey; 152 | try { 153 | final MessageDigest mDigest = MessageDigest.getInstance("MD5"); 154 | mDigest.update(key.getBytes()); 155 | cacheKey = bytesToHexString(mDigest.digest()); 156 | } catch (NoSuchAlgorithmException e) { 157 | cacheKey = String.valueOf(key.hashCode()); 158 | } 159 | return cacheKey; 160 | } 161 | 162 | private String bytesToHexString(byte[] bytes) { 163 | StringBuilder sb = new StringBuilder(); 164 | for (int i = 0; i < bytes.length; i++) { 165 | String hex = Integer.toHexString(0xFF & bytes[i]); 166 | if (hex.length() == 1) { 167 | sb.append('0'); 168 | } 169 | sb.append(hex); 170 | } 171 | return sb.toString(); 172 | } 173 | public File getDiskCacheDir(Context context, String uniqueName) { 174 | String cachePath; 175 | if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) 176 | || !Environment.isExternalStorageRemovable()) { 177 | cachePath = context.getExternalCacheDir().getPath(); 178 | } else { 179 | cachePath = context.getCacheDir().getPath(); 180 | } 181 | return new File(cachePath + File.separator + uniqueName); 182 | } 183 | 184 | public int getAppVersion(Context context) { 185 | try { 186 | PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); 187 | return info.versionCode; 188 | } catch (PackageManager.NameNotFoundException e) { 189 | e.printStackTrace(); 190 | } 191 | return 1; 192 | } 193 | 194 | 195 | } 196 | -------------------------------------------------------------------------------- /app/src/main/java/com/xc/xccache/DiskLruCache.java: -------------------------------------------------------------------------------- 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.xc.xccache; 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 | *

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 | *

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 | *

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 | *

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 | *

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 | *

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 lruEntries 163 | = new LinkedHashMap(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[] 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 "\r\n", or 210 | * "\n". 211 | * 212 | * @throws 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 == '\n') { 224 | break; 225 | } 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 - 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()); 272 | private final Callable cleanupCallable = new Callable() { 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 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 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("\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 | for (Entry entry : lruEntries.values()) { 443 | if (entry.currentEditor != null) { 444 | writer.write(DIRTY + ' ' + entry.key + '\n'); 445 | } else { 446 | writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n'); 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 + '\n'); 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 + '\n'); 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() + '\n'); 602 | if (success) { 603 | entry.sequenceNumber = nextSequenceNumber++; 604 | } 605 | } else { 606 | lruEntries.remove(entry.key); 607 | journalWriter.write(REMOVE + ' ' + entry.key + '\n'); 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 + '\n'); 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(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 toEvict = lruEntries.eldest(); 701 | final Map.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("\n") || key.contains("\r")) { 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 | } 954 | -------------------------------------------------------------------------------- /app/src/main/java/com/xc/xccache/MemoryCache.java: -------------------------------------------------------------------------------- 1 | package com.xc.xccache; 2 | 3 | import android.support.v4.util.LruCache; 4 | 5 | /** 6 | * 内存缓存类 7 | * Created by caizhiming on 2015/12/4. 8 | */ 9 | public class MemoryCache implements Cache { 10 | private LruCache mMemoryLruCache; 11 | private EvictedListener mEvictedListener; 12 | 13 | public MemoryCache() { 14 | init(); 15 | } 16 | 17 | public MemoryCache(EvictedListener listener) { 18 | init(); 19 | this.mEvictedListener = listener; 20 | } 21 | 22 | public void setEvictedListener(EvictedListener listener) { 23 | this.mEvictedListener = listener; 24 | } 25 | 26 | public boolean hasEvictedListener() { 27 | return mEvictedListener != null; 28 | } 29 | 30 | private void init() { 31 | // 计算可使用的最大内存 32 | final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024); 33 | // 取可用内存空间的1/4作为缓存 34 | final int cacheSize = maxMemory / 4; 35 | mMemoryLruCache = new LruCache(cacheSize) { 36 | @Override 37 | protected int sizeOf(String key, String value) { 38 | return value.getBytes().length; 39 | } 40 | 41 | @Override 42 | protected void entryRemoved(boolean evicted, String key, String oldValue, String newValue) { 43 | if (evicted) { 44 | if (mEvictedListener != null) { 45 | mEvictedListener.handleEvictEntry(key, oldValue); 46 | } 47 | } 48 | } 49 | }; 50 | } 51 | 52 | @Override 53 | public String get(String key) { 54 | return mMemoryLruCache.get(key); 55 | } 56 | 57 | @Override 58 | public void put(String key, String value) { 59 | mMemoryLruCache.put(key, value); 60 | } 61 | 62 | @Override 63 | public boolean remove(String key) { 64 | return Boolean.parseBoolean(mMemoryLruCache.remove(key)); 65 | } 66 | 67 | /** 68 | * called when mMemoryLruCache evict entrys, 69 | *

70 | * using by CacheManager.Strategy.MEMORY_FIRST 71 | */ 72 | public interface EvictedListener { 73 | void handleEvictEntry(String evictKey, String evictValue); 74 | } 75 | 76 | } 77 | -------------------------------------------------------------------------------- /app/src/main/java/com/xc/xccache/XCCacheManager.java: -------------------------------------------------------------------------------- 1 | package com.xc.xccache; 2 | 3 | import android.content.Context; 4 | 5 | import java.util.concurrent.Callable; 6 | import java.util.concurrent.ExecutionException; 7 | import java.util.concurrent.ExecutorService; 8 | import java.util.concurrent.Executors; 9 | import java.util.concurrent.Future; 10 | 11 | /** 12 | * Created by caizhiming on 2015/11/27. 13 | * 使用内存缓存和Disk缓存双缓存的Http缓存管理类 14 | */ 15 | public class XCCacheManager { 16 | 17 | private static XCCacheManager mInstance = null; 18 | 19 | private Strategy mStrategy = Strategy.MEMORY_FIRST; 20 | //线程池 21 | private ExecutorService mExecutor = null; 22 | //内存缓存 23 | private MemoryCache mMemoryCache; 24 | //Disk缓存 25 | private DiskCache mDiskCache; 26 | 27 | public static XCCacheManager getInstance(Context context) { 28 | return getInstance(context, Strategy.MEMORY_FIRST); 29 | } 30 | 31 | public static XCCacheManager getInstance(Context context, Strategy strategy) { 32 | if (mInstance == null) { 33 | synchronized (XCCacheManager.class) { 34 | if (mInstance == null) { 35 | mInstance = new XCCacheManager(context.getApplicationContext(), strategy); 36 | } 37 | } 38 | } else { 39 | mInstance.setStrategy(strategy); 40 | } 41 | return mInstance; 42 | } 43 | 44 | private XCCacheManager(Context context, Strategy strategy) { 45 | this.mStrategy = strategy; 46 | init(context); 47 | } 48 | 49 | public void setStrategy(XCCacheManager.Strategy strategy) { 50 | this.mStrategy = strategy; 51 | switch (mStrategy) { 52 | case MEMORY_FIRST: 53 | if (!mMemoryCache.hasEvictedListener()) { 54 | mMemoryCache.setEvictedListener(new MemoryCache.EvictedListener() { 55 | @Override 56 | public void handleEvictEntry(String evictKey, String evictValue) { 57 | mDiskCache.put(evictKey, evictValue); 58 | } 59 | }); 60 | } 61 | break; 62 | case MEMORY_ONLY: 63 | if (mMemoryCache.hasEvictedListener()) 64 | mMemoryCache.setEvictedListener(null); 65 | break; 66 | case DISK_ONLY: 67 | break; 68 | } 69 | } 70 | 71 | public String getCurStrategy() { 72 | return mStrategy.name(); 73 | } 74 | 75 | /** 76 | * 初始化 DiskLruCache 77 | */ 78 | private void init(Context context) { 79 | mExecutor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); 80 | mDiskCache = new DiskCache(context); 81 | mMemoryCache = new MemoryCache(); 82 | } 83 | 84 | /** 85 | * 从缓存中读取value 86 | */ 87 | public String readCache(final String key) { 88 | Future ret = mExecutor.submit(new Callable() { 89 | @Override 90 | public String call() throws Exception { 91 | String result = null; 92 | switch (mStrategy) { 93 | case MEMORY_ONLY: 94 | result = mMemoryCache.get(key); 95 | break; 96 | case MEMORY_FIRST: 97 | result = mMemoryCache.get(key); 98 | if (result == null) { 99 | result = mDiskCache.get(key); 100 | } 101 | break; 102 | case DISK_ONLY: 103 | result = mDiskCache.get(key); 104 | break; 105 | } 106 | return result; 107 | } 108 | }); 109 | try { 110 | return ret.get(); 111 | } catch (InterruptedException e) { 112 | e.printStackTrace(); 113 | } catch (ExecutionException e) { 114 | e.printStackTrace(); 115 | } 116 | return null; 117 | } 118 | 119 | /** 120 | * 将value 写入到缓存中 121 | */ 122 | public void writeCache(final String key, final String value) { 123 | mExecutor.submit(new Runnable() { 124 | @Override 125 | public void run() { 126 | switch (mStrategy) { 127 | case MEMORY_FIRST: 128 | mMemoryCache.put(key, value); 129 | mDiskCache.put(key,value); 130 | break; 131 | case MEMORY_ONLY: 132 | mMemoryCache.put(key, value); 133 | break; 134 | case DISK_ONLY: 135 | mDiskCache.put(key, value); 136 | break; 137 | } 138 | } 139 | }); 140 | } 141 | 142 | enum Strategy { 143 | MEMORY_ONLY(0), MEMORY_FIRST(1), DISK_ONLY(3); 144 | int id; 145 | 146 | Strategy(int id) { 147 | this.id = id; 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /app/src/main/java/com/xc/xccachemanager/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.xc.xccachemanager; 2 | 3 | import android.app.Activity; 4 | import android.os.Bundle; 5 | import android.view.View; 6 | import android.widget.Button; 7 | import android.widget.EditText; 8 | import android.widget.TextView; 9 | import android.widget.Toast; 10 | 11 | import com.xc.xccache.XCCacheManager; 12 | 13 | public class MainActivity extends Activity implements View.OnClickListener { 14 | private Button mBtnWrite; 15 | private Button mBtnRead; 16 | private EditText mEtText; 17 | private TextView mTvResult; 18 | 19 | private XCCacheManager mCacheManager; 20 | 21 | @Override 22 | protected void onCreate(Bundle savedInstanceState) { 23 | super.onCreate(savedInstanceState); 24 | setContentView(R.layout.activity_main); 25 | mBtnRead = (Button) findViewById(R.id.btn_read); 26 | mBtnWrite = (Button) findViewById(R.id.btn_write); 27 | mEtText = (EditText) findViewById(R.id.et_text); 28 | mTvResult = (TextView) findViewById(R.id.tv_result); 29 | mBtnWrite.setOnClickListener(this); 30 | mBtnRead.setOnClickListener(this); 31 | 32 | mCacheManager = XCCacheManager.getInstance(this); 33 | } 34 | 35 | @Override 36 | public void onClick(View v) { 37 | switch (v.getId()){ 38 | case R.id.btn_read: 39 | mTvResult.setText(mCacheManager.readCache("key_demo")); 40 | break; 41 | case R.id.btn_write: 42 | mCacheManager.writeCache("key_demo",mEtText.getText().toString()); 43 | Toast.makeText(this,"write string to cache",Toast.LENGTH_SHORT).show(); 44 | break; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 15 |