├── .classpath ├── .gitignore ├── .project ├── .settings └── org.eclipse.jdt.core.prefs ├── AndroidManifest.xml ├── README.md ├── ic_launcher-web.png ├── libs ├── android-support-v4.jar └── fastjson-1.1.34.android.jar ├── proguard-project.txt ├── project.properties ├── res ├── drawable-hdpi │ └── ic_launcher.png ├── drawable-mdpi │ └── ic_launcher.png ├── drawable-xhdpi │ └── ic_launcher.png ├── drawable-xxhdpi │ └── ic_launcher.png ├── layout │ ├── activity_main.xml │ └── list_item.xml ├── menu │ └── main.xml ├── values-sw600dp │ └── dimens.xml ├── values-sw720dp-land │ └── dimens.xml ├── values-v11 │ └── styles.xml ├── values-v14 │ └── styles.xml └── values │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml └── src └── com └── leslie └── demo ├── AsyncImageLoader.java ├── DiskLruCache.java ├── MainActivity.java └── MyAdapter.java /.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # built application files 2 | *.apk 3 | *.ap_ 4 | .DS_Store 5 | 6 | # files for the dex VM 7 | *.dex 8 | 9 | # Java class files 10 | *.class 11 | 12 | # generated files 13 | bin/ 14 | gen/ 15 | 16 | # Ignore gradle files 17 | .gradle/ 18 | build/ 19 | 20 | # Local configuration file (sdk path, etc) 21 | local.properties 22 | .idea -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | AsyncImageLoaderDemo 4 | 5 | 6 | 7 | 8 | 9 | com.android.ide.eclipse.adt.ResourceManagerBuilder 10 | 11 | 12 | 13 | 14 | com.android.ide.eclipse.adt.PreCompilerBuilder 15 | 16 | 17 | 18 | 19 | org.eclipse.jdt.core.javabuilder 20 | 21 | 22 | 23 | 24 | com.android.ide.eclipse.adt.ApkBuilder 25 | 26 | 27 | 28 | 29 | 30 | com.android.ide.eclipse.adt.AndroidNature 31 | org.eclipse.jdt.core.javanature 32 | 33 | 34 | -------------------------------------------------------------------------------- /.settings/org.eclipse.jdt.core.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled 3 | org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 4 | org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve 5 | org.eclipse.jdt.core.compiler.compliance=1.6 6 | org.eclipse.jdt.core.compiler.debug.lineNumber=generate 7 | org.eclipse.jdt.core.compiler.debug.localVariable=generate 8 | org.eclipse.jdt.core.compiler.debug.sourceFile=generate 9 | org.eclipse.jdt.core.compiler.problem.assertIdentifier=error 10 | org.eclipse.jdt.core.compiler.problem.enumIdentifier=error 11 | org.eclipse.jdt.core.compiler.source=1.6 12 | -------------------------------------------------------------------------------- /AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 18 | 19 | 24 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | AsyncImageLoader 2 | ================ 3 | 4 | android listview 异步加载图片并防止错位 demo,内存文件二级缓存。 5 | 内存缓存基于 LruCache 6 | 文件缓存基于 DiskLruCache 7 | -------------------------------------------------------------------------------- /ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lesliebeijing/AsyncImageLoader/ccff454432e2f4302fccaa8ec28092f5d1713025/ic_launcher-web.png -------------------------------------------------------------------------------- /libs/android-support-v4.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lesliebeijing/AsyncImageLoader/ccff454432e2f4302fccaa8ec28092f5d1713025/libs/android-support-v4.jar -------------------------------------------------------------------------------- /libs/fastjson-1.1.34.android.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lesliebeijing/AsyncImageLoader/ccff454432e2f4302fccaa8ec28092f5d1713025/libs/fastjson-1.1.34.android.jar -------------------------------------------------------------------------------- /proguard-project.txt: -------------------------------------------------------------------------------- 1 | # To enable ProGuard in your project, edit project.properties 2 | # to define the proguard.config property as described in that file. 3 | # 4 | # Add project specific ProGuard rules here. 5 | # By default, the flags in this file are appended to flags specified 6 | # in ${sdk.dir}/tools/proguard/proguard-android.txt 7 | # You can edit the include path and order by changing the ProGuard 8 | # include property in project.properties. 9 | # 10 | # For more details, see 11 | # http://developer.android.com/guide/developing/tools/proguard.html 12 | 13 | # Add any project specific keep options here: 14 | 15 | # If your project uses WebView with JS, uncomment the following 16 | # and specify the fully qualified class name to the JavaScript interface 17 | # class: 18 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 19 | # public *; 20 | #} 21 | -------------------------------------------------------------------------------- /project.properties: -------------------------------------------------------------------------------- 1 | # This file is automatically generated by Android Tools. 2 | # Do not modify this file -- YOUR CHANGES WILL BE ERASED! 3 | # 4 | # This file must be checked in Version Control Systems. 5 | # 6 | # To customize properties used by the Ant build system edit 7 | # "ant.properties", and override values to adapt the script to your 8 | # project structure. 9 | # 10 | # To enable ProGuard to shrink and obfuscate your code, uncomment this (available properties: sdk.dir, user.home): 11 | #proguard.config=${sdk.dir}/tools/proguard/proguard-android.txt:proguard-project.txt 12 | 13 | # Project target. 14 | target=android-17 15 | -------------------------------------------------------------------------------- /res/drawable-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lesliebeijing/AsyncImageLoader/ccff454432e2f4302fccaa8ec28092f5d1713025/res/drawable-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /res/drawable-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lesliebeijing/AsyncImageLoader/ccff454432e2f4302fccaa8ec28092f5d1713025/res/drawable-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /res/drawable-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lesliebeijing/AsyncImageLoader/ccff454432e2f4302fccaa8ec28092f5d1713025/res/drawable-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /res/drawable-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lesliebeijing/AsyncImageLoader/ccff454432e2f4302fccaa8ec28092f5d1713025/res/drawable-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /res/layout/list_item.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | 12 | -------------------------------------------------------------------------------- /res/menu/main.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /res/values-sw600dp/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /res/values-sw720dp-land/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 128dp 8 | 9 | 10 | -------------------------------------------------------------------------------- /res/values-v11/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /res/values-v14/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16dp 5 | 16dp 6 | 7 | 8 | -------------------------------------------------------------------------------- /res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | AsyncImageLoader 5 | Settings 6 | Hello world! 7 | 8 | 9 | -------------------------------------------------------------------------------- /res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 14 | 15 | 16 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/com/leslie/demo/AsyncImageLoader.java: -------------------------------------------------------------------------------- 1 | package com.leslie.demo; 2 | 3 | import java.io.BufferedInputStream; 4 | import java.io.BufferedOutputStream; 5 | import java.io.File; 6 | import java.io.IOException; 7 | import java.io.InputStream; 8 | import java.io.OutputStream; 9 | import java.net.HttpURLConnection; 10 | import java.net.URL; 11 | import java.security.MessageDigest; 12 | import java.security.NoSuchAlgorithmException; 13 | 14 | import android.content.Context; 15 | import android.content.pm.PackageInfo; 16 | import android.content.pm.PackageManager.NameNotFoundException; 17 | import android.graphics.Bitmap; 18 | import android.graphics.BitmapFactory; 19 | import android.os.AsyncTask; 20 | import android.os.Environment; 21 | import android.support.v4.util.LruCache; 22 | import android.text.TextUtils; 23 | import android.util.Log; 24 | import android.widget.ImageView; 25 | 26 | /** 27 | * 图片异步加载类 28 | * 29 | * @author Leslie.Fang 30 | * 31 | */ 32 | public class AsyncImageLoader { 33 | private Context context; 34 | // 内存缓存默认 5M 35 | static final int MEM_CACHE_DEFAULT_SIZE = 5 * 1024 * 1024; 36 | // 文件缓存默认 10M 37 | static final int DISK_CACHE_DEFAULT_SIZE = 10 * 1024 * 1024; 38 | // 一级内存缓存基于 LruCache 39 | private LruCache memCache; 40 | // 二级文件缓存基于 DiskLruCache 41 | private DiskLruCache diskCache; 42 | 43 | public AsyncImageLoader(Context context) { 44 | this.context = context; 45 | initMemCache(); 46 | initDiskLruCache(); 47 | } 48 | 49 | /** 50 | * 初始化内存缓存 51 | */ 52 | private void initMemCache() { 53 | memCache = new LruCache(MEM_CACHE_DEFAULT_SIZE) { 54 | @Override 55 | protected int sizeOf(String key, Bitmap bitmap) { 56 | return bitmap.getByteCount(); 57 | } 58 | }; 59 | } 60 | 61 | /** 62 | * 初始化文件缓存 63 | */ 64 | private void initDiskLruCache() { 65 | try { 66 | File cacheDir = getDiskCacheDir(context, "bitmap"); 67 | if (!cacheDir.exists()) { 68 | cacheDir.mkdirs(); 69 | } 70 | diskCache = DiskLruCache.open(cacheDir, getAppVersion(context), 1, DISK_CACHE_DEFAULT_SIZE); 71 | } catch (IOException e) { 72 | e.printStackTrace(); 73 | } 74 | } 75 | 76 | /** 77 | * 从内存缓存中拿 78 | * 79 | * @param url 80 | */ 81 | public Bitmap getBitmapFromMem(String url) { 82 | return memCache.get(url); 83 | } 84 | 85 | /** 86 | * 加入到内存缓存中 87 | * 88 | * @param url 89 | * @param bitmap 90 | */ 91 | public void putBitmapToMem(String url, Bitmap bitmap) { 92 | memCache.put(url, bitmap); 93 | } 94 | 95 | /** 96 | * 从文件缓存中拿 97 | * 98 | * @param url 99 | */ 100 | public Bitmap getBitmapFromDisk(String url) { 101 | try { 102 | String key = hashKeyForDisk(url); 103 | DiskLruCache.Snapshot snapShot = diskCache.get(key); 104 | if (snapShot != null) { 105 | InputStream is = snapShot.getInputStream(0); 106 | Bitmap bitmap = BitmapFactory.decodeStream(is); 107 | return bitmap; 108 | } 109 | } catch (IOException e) { 110 | e.printStackTrace(); 111 | } 112 | 113 | return null; 114 | } 115 | 116 | /** 117 | * 从 url 加载图片 118 | * 119 | * @param imageView 120 | * @param imageUrl 121 | */ 122 | public Bitmap loadImage(ImageView imageView, String imageUrl) { 123 | // 先从内存中拿 124 | Bitmap bitmap = getBitmapFromMem(imageUrl); 125 | 126 | if (bitmap != null) { 127 | Log.i("leslie", "image exists in memory"); 128 | return bitmap; 129 | } 130 | 131 | // 再从文件中找 132 | bitmap = getBitmapFromDisk(imageUrl); 133 | if (bitmap != null) { 134 | Log.i("leslie", "image exists in file"); 135 | // 重新缓存到内存中 136 | putBitmapToMem(imageUrl, bitmap); 137 | return bitmap; 138 | } 139 | 140 | // 内存和文件中都没有再从网络下载 141 | if (!TextUtils.isEmpty(imageUrl)) { 142 | new ImageDownloadTask(imageView).execute(imageUrl); 143 | } 144 | 145 | return null; 146 | } 147 | 148 | class ImageDownloadTask extends AsyncTask { 149 | private String imageUrl; 150 | private ImageView imageView; 151 | 152 | public ImageDownloadTask(ImageView imageView) { 153 | this.imageView = imageView; 154 | } 155 | 156 | @Override 157 | protected Bitmap doInBackground(String... params) { 158 | try { 159 | imageUrl = params[0]; 160 | String key = hashKeyForDisk(imageUrl); 161 | // 下载成功后直接将图片流写入文件缓存 162 | DiskLruCache.Editor editor = diskCache.edit(key); 163 | if (editor != null) { 164 | OutputStream outputStream = editor.newOutputStream(0); 165 | if (downloadUrlToStream(imageUrl, outputStream)) { 166 | editor.commit(); 167 | } else { 168 | editor.abort(); 169 | } 170 | } 171 | diskCache.flush(); 172 | 173 | Bitmap bitmap = getBitmapFromDisk(imageUrl); 174 | if (bitmap != null) { 175 | // 将图片加入到内存缓存中 176 | putBitmapToMem(imageUrl, bitmap); 177 | } 178 | 179 | return bitmap; 180 | } catch (IOException e) { 181 | e.printStackTrace(); 182 | } 183 | 184 | return null; 185 | } 186 | 187 | @Override 188 | protected void onPostExecute(Bitmap result) { 189 | super.onPostExecute(result); 190 | if (result != null) { 191 | // 通过 tag 来防止图片错位 192 | if (imageView.getTag() != null && imageView.getTag().equals(imageUrl)) { 193 | imageView.setImageBitmap(result); 194 | } 195 | } 196 | } 197 | 198 | private boolean downloadUrlToStream(String urlString, OutputStream outputStream) { 199 | HttpURLConnection urlConnection = null; 200 | BufferedOutputStream out = null; 201 | BufferedInputStream in = null; 202 | try { 203 | final URL url = new URL(urlString); 204 | urlConnection = (HttpURLConnection) url.openConnection(); 205 | in = new BufferedInputStream(urlConnection.getInputStream(), 8 * 1024); 206 | out = new BufferedOutputStream(outputStream, 8 * 1024); 207 | int b; 208 | while ((b = in.read()) != -1) { 209 | out.write(b); 210 | } 211 | return true; 212 | } catch (final IOException e) { 213 | e.printStackTrace(); 214 | } finally { 215 | if (urlConnection != null) { 216 | urlConnection.disconnect(); 217 | } 218 | try { 219 | if (out != null) { 220 | out.close(); 221 | } 222 | if (in != null) { 223 | in.close(); 224 | } 225 | } catch (final IOException e) { 226 | e.printStackTrace(); 227 | } 228 | } 229 | return false; 230 | } 231 | } 232 | 233 | private File getDiskCacheDir(Context context, String uniqueName) { 234 | String cachePath; 235 | if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) 236 | || !Environment.isExternalStorageRemovable()) { 237 | cachePath = context.getExternalCacheDir().getPath(); 238 | } else { 239 | cachePath = context.getCacheDir().getPath(); 240 | } 241 | return new File(cachePath + File.separator + uniqueName); 242 | } 243 | 244 | private int getAppVersion(Context context) { 245 | try { 246 | PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0); 247 | return info.versionCode; 248 | } catch (NameNotFoundException e) { 249 | e.printStackTrace(); 250 | } 251 | return 1; 252 | } 253 | 254 | private String hashKeyForDisk(String key) { 255 | String cacheKey; 256 | try { 257 | final MessageDigest mDigest = MessageDigest.getInstance("MD5"); 258 | mDigest.update(key.getBytes()); 259 | cacheKey = bytesToHexString(mDigest.digest()); 260 | } catch (NoSuchAlgorithmException e) { 261 | cacheKey = String.valueOf(key.hashCode()); 262 | } 263 | return cacheKey; 264 | } 265 | 266 | private String bytesToHexString(byte[] bytes) { 267 | StringBuilder sb = new StringBuilder(); 268 | for (int i = 0; i < bytes.length; i++) { 269 | String hex = Integer.toHexString(0xFF & bytes[i]); 270 | if (hex.length() == 1) { 271 | sb.append('0'); 272 | } 273 | sb.append(hex); 274 | } 275 | return sb.toString(); 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /src/com/leslie/demo/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.leslie.demo; 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 | *

    79 | *
  • When an entry is being created it is necessary to 80 | * supply a full set of values; the empty value should be used as a 81 | * placeholder if necessary. 82 | *
  • When an entry is being edited, it is not necessary 83 | * to supply data for every value; values default to their previous 84 | * value. 85 | *
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 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 == '\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 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 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 | -------------------------------------------------------------------------------- /src/com/leslie/demo/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.leslie.demo; 2 | 3 | import android.app.Activity; 4 | import android.os.Bundle; 5 | import android.widget.ListView; 6 | 7 | public class MainActivity extends Activity { 8 | private ListView listview; 9 | 10 | @Override 11 | protected void onCreate(Bundle savedInstanceState) { 12 | super.onCreate(savedInstanceState); 13 | setContentView(R.layout.activity_main); 14 | 15 | String[] images = new String[] { "http://img.my.csdn.net/uploads/201407/26/1406383299_1976.jpg", 16 | "http://img.my.csdn.net/uploads/201407/26/1406383291_6518.jpg", 17 | "http://img.my.csdn.net/uploads/201407/26/1406383291_8239.jpg", 18 | "http://img.my.csdn.net/uploads/201407/26/1406383290_9329.jpg", 19 | "http://img.my.csdn.net/uploads/201407/26/1406383290_1042.jpg", 20 | "http://img.my.csdn.net/uploads/201407/26/1406383275_3977.jpg", 21 | "http://img.my.csdn.net/uploads/201407/26/1406383265_8550.jpg", 22 | "http://img.my.csdn.net/uploads/201407/26/1406383264_3954.jpg", 23 | "http://img.my.csdn.net/uploads/201407/26/1406383264_4787.jpg", 24 | "http://img.my.csdn.net/uploads/201407/26/1406383264_8243.jpg", 25 | "http://img.my.csdn.net/uploads/201407/26/1406383248_3693.jpg", 26 | "http://img.my.csdn.net/uploads/201407/26/1406383243_5120.jpg", 27 | "http://img.my.csdn.net/uploads/201407/26/1406383242_3127.jpg", 28 | "http://img.my.csdn.net/uploads/201407/26/1406383242_9576.jpg", 29 | "http://img.my.csdn.net/uploads/201407/26/1406383242_1721.jpg", 30 | "http://img.my.csdn.net/uploads/201407/26/1406383219_5806.jpg", 31 | "http://img.my.csdn.net/uploads/201407/26/1406383214_7794.jpg", 32 | "http://img.my.csdn.net/uploads/201407/26/1406383213_4418.jpg", 33 | "http://img.my.csdn.net/uploads/201407/26/1406383213_3557.jpg", 34 | "http://img.my.csdn.net/uploads/201407/26/1406383210_8779.jpg", 35 | "http://img.my.csdn.net/uploads/201407/26/1406383172_4577.jpg", 36 | "http://img.my.csdn.net/uploads/201407/26/1406383166_3407.jpg", 37 | "http://img.my.csdn.net/uploads/201407/26/1406383166_2224.jpg", 38 | "http://img.my.csdn.net/uploads/201407/26/1406383166_7301.jpg", 39 | "http://img.my.csdn.net/uploads/201407/26/1406383165_7197.jpg", 40 | "http://img.my.csdn.net/uploads/201407/26/1406383150_8410.jpg", 41 | "http://img.my.csdn.net/uploads/201407/26/1406383131_3736.jpg", 42 | "http://img.my.csdn.net/uploads/201407/26/1406383130_5094.jpg", 43 | "http://img.my.csdn.net/uploads/201407/26/1406383130_7393.jpg", 44 | "http://img.my.csdn.net/uploads/201407/26/1406383129_8813.jpg", 45 | "http://img.my.csdn.net/uploads/201407/26/1406383100_3554.jpg", 46 | "http://img.my.csdn.net/uploads/201407/26/1406383093_7894.jpg", 47 | "http://img.my.csdn.net/uploads/201407/26/1406383092_2432.jpg", 48 | "http://img.my.csdn.net/uploads/201407/26/1406383092_3071.jpg", 49 | "http://img.my.csdn.net/uploads/201407/26/1406383091_3119.jpg", 50 | "http://img.my.csdn.net/uploads/201407/26/1406383059_6589.jpg", 51 | "http://img.my.csdn.net/uploads/201407/26/1406383059_8814.jpg", 52 | "http://img.my.csdn.net/uploads/201407/26/1406383059_2237.jpg", 53 | "http://img.my.csdn.net/uploads/201407/26/1406383058_4330.jpg", 54 | "http://img.my.csdn.net/uploads/201407/26/1406383038_3602.jpg", 55 | "http://img.my.csdn.net/uploads/201407/26/1406382942_3079.jpg", 56 | "http://img.my.csdn.net/uploads/201407/26/1406382942_8125.jpg", 57 | "http://img.my.csdn.net/uploads/201407/26/1406382942_4881.jpg", 58 | "http://img.my.csdn.net/uploads/201407/26/1406382941_4559.jpg", 59 | "http://img.my.csdn.net/uploads/201407/26/1406382941_3845.jpg", 60 | "http://img.my.csdn.net/uploads/201407/26/1406382924_8955.jpg", 61 | "http://img.my.csdn.net/uploads/201407/26/1406382923_2141.jpg", 62 | "http://img.my.csdn.net/uploads/201407/26/1406382923_8437.jpg", 63 | "http://img.my.csdn.net/uploads/201407/26/1406382922_6166.jpg", 64 | "http://img.my.csdn.net/uploads/201407/26/1406382922_4843.jpg", 65 | "http://img.my.csdn.net/uploads/201407/26/1406382905_5804.jpg", 66 | "http://img.my.csdn.net/uploads/201407/26/1406382904_3362.jpg", 67 | "http://img.my.csdn.net/uploads/201407/26/1406382904_2312.jpg", 68 | "http://img.my.csdn.net/uploads/201407/26/1406382904_4960.jpg", 69 | "http://img.my.csdn.net/uploads/201407/26/1406382900_2418.jpg", 70 | "http://img.my.csdn.net/uploads/201407/26/1406382881_4490.jpg", 71 | "http://img.my.csdn.net/uploads/201407/26/1406382881_5935.jpg", 72 | "http://img.my.csdn.net/uploads/201407/26/1406382880_3865.jpg", 73 | "http://img.my.csdn.net/uploads/201407/26/1406382880_4662.jpg", 74 | "http://img.my.csdn.net/uploads/201407/26/1406382879_2553.jpg", 75 | "http://img.my.csdn.net/uploads/201407/26/1406382862_5375.jpg", 76 | "http://img.my.csdn.net/uploads/201407/26/1406382862_1748.jpg", 77 | "http://img.my.csdn.net/uploads/201407/26/1406382861_7618.jpg", 78 | "http://img.my.csdn.net/uploads/201407/26/1406382861_8606.jpg", 79 | "http://img.my.csdn.net/uploads/201407/26/1406382861_8949.jpg", 80 | "http://img.my.csdn.net/uploads/201407/26/1406382841_9821.jpg", 81 | "http://img.my.csdn.net/uploads/201407/26/1406382840_6603.jpg", 82 | "http://img.my.csdn.net/uploads/201407/26/1406382840_2405.jpg", 83 | "http://img.my.csdn.net/uploads/201407/26/1406382840_6354.jpg", 84 | "http://img.my.csdn.net/uploads/201407/26/1406382839_5779.jpg", 85 | "http://img.my.csdn.net/uploads/201407/26/1406382810_7578.jpg", 86 | "http://img.my.csdn.net/uploads/201407/26/1406382810_2436.jpg", 87 | "http://img.my.csdn.net/uploads/201407/26/1406382809_3883.jpg", 88 | "http://img.my.csdn.net/uploads/201407/26/1406382809_6269.jpg", 89 | "http://img.my.csdn.net/uploads/201407/26/1406382808_4179.jpg", 90 | "http://img.my.csdn.net/uploads/201407/26/1406382790_8326.jpg", 91 | "http://img.my.csdn.net/uploads/201407/26/1406382789_7174.jpg", 92 | "http://img.my.csdn.net/uploads/201407/26/1406382789_5170.jpg", 93 | "http://img.my.csdn.net/uploads/201407/26/1406382789_4118.jpg", 94 | "http://img.my.csdn.net/uploads/201407/26/1406382788_9532.jpg", 95 | "http://img.my.csdn.net/uploads/201407/26/1406382767_3184.jpg", 96 | "http://img.my.csdn.net/uploads/201407/26/1406382767_4772.jpg", 97 | "http://img.my.csdn.net/uploads/201407/26/1406382766_4924.jpg", 98 | "http://img.my.csdn.net/uploads/201407/26/1406382766_5762.jpg", 99 | "http://img.my.csdn.net/uploads/201407/26/1406382765_7341.jpg" }; 100 | 101 | listview = (ListView) findViewById(R.id.listview); 102 | MyAdapter adapter = new MyAdapter(this, images); 103 | listview.setAdapter(adapter); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/com/leslie/demo/MyAdapter.java: -------------------------------------------------------------------------------- 1 | package com.leslie.demo; 2 | 3 | import android.content.Context; 4 | import android.graphics.Bitmap; 5 | import android.text.TextUtils; 6 | import android.view.LayoutInflater; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | import android.widget.BaseAdapter; 10 | import android.widget.ImageView; 11 | 12 | public class MyAdapter extends BaseAdapter { 13 | private String[] list; 14 | private Context context; 15 | private AsyncImageLoader imageLoader; 16 | 17 | public MyAdapter(Context context, String[] list) { 18 | this.context = context; 19 | this.list = list; 20 | imageLoader = new AsyncImageLoader(context); 21 | } 22 | 23 | @Override 24 | public int getCount() { 25 | return list.length; 26 | } 27 | 28 | @Override 29 | public Object getItem(int position) { 30 | return list[position]; 31 | } 32 | 33 | @Override 34 | public long getItemId(int position) { 35 | return position; 36 | } 37 | 38 | @Override 39 | public View getView(int position, View convertView, ViewGroup parent) { 40 | ViewHolder holder = null; 41 | 42 | if (convertView == null) { 43 | holder = new ViewHolder(); 44 | convertView = LayoutInflater.from(context).inflate(R.layout.list_item, null); 45 | holder.img = (ImageView) convertView.findViewById(R.id.userimage); 46 | 47 | convertView.setTag(holder); 48 | } else { 49 | holder = (ViewHolder) convertView.getTag(); 50 | } 51 | 52 | final String imgUrl = list[position]; 53 | // 给 ImageView 设置一个 tag 54 | holder.img.setTag(imgUrl); 55 | // 预设一个图片 56 | holder.img.setImageResource(R.drawable.ic_launcher); 57 | 58 | if (!TextUtils.isEmpty(imgUrl)) { 59 | Bitmap bitmap = imageLoader.loadImage(holder.img, imgUrl); 60 | if (bitmap != null) { 61 | holder.img.setImageBitmap(bitmap); 62 | } 63 | } 64 | 65 | return convertView; 66 | } 67 | 68 | class ViewHolder { 69 | ImageView img; 70 | } 71 | } 72 | --------------------------------------------------------------------------------