图片加载器在Android的App中可谓是重中之重,几乎任何一款APP都缺少不了图片,而图片可以使一个APP具有更丰富的内容,同时反应一个APP具有更加友好的用户体验。一般使用的图片都需要到网络上拉去回来,这就需要图片加载器了。

一个好的图片加载器能够不影响APP性能,同时也应具有以下功能:

1、图片的同步加载和异步加载

2、图片压缩

3、内存缓存和磁盘缓存

4、网络拉取

从以上的功能特点得出,打造一个优秀的图片加载器,需要的Bitmap、网络和图片缓存知识等等。

接下来我们一边打造图片加载器一边讲解Android的缓存策略。

定义缓存接口
public interface ImageCache
{public void setCache(String key,Bitmap bitmap);public Bitmap getCache(String key,int reqWidth, int reqHeight);
}
复制代码

该接口功能很简单:

1)根据key,设置bitmap的缓存;

2)根据key,获取指定大小的bitmap缓存。

实现内存缓存

Android中常用的缓存算法就是LRU(Least Recently Used),即近期最少使用算法。LRU算法有两种:LruCache和DiskLruCache,而内存缓存使用LruCache。

LruCache是Android3.1提供的一个缓存类,通过support-v4兼容包,可以兼容到Android2.2版本。

import android.graphics.Bitmap;
import android.support.v4.util.LruCache;public class MemoryCache implements ImageCache
{private LruCache<String, Bitmap> mMemoryCache;public MemoryCache(){initImageCache();}private void initImageCache(){//计算可使用的最大内存int maxSize = (int)(Runtime.getRuntime().maxMemory() / 1024);int cacheSize = maxSize / 8;mMemoryCache = new LruCache<String, Bitmap>(cacheSize){@Overrideprotected int sizeOf(String key, Bitmap value){return value.getRowBytes() * value.getHeight() /1024;}};}@Overridepublic void setCache(String url, Bitmap bitmap){String key = BitmapUtils.hashKeyFormUrl(url);if(mMemoryCache.get(key) == null){mMemoryCache.put(key, bitmap);}}@Overridepublic Bitmap getCache(String url, int reqWidth, int reqHeight){String key = BitmapUtils.hashKeyFormUrl(url);return mMemoryCache.get(key);}
}
复制代码

为了兼容到Android2.2版本,这里我使用support-v4包下的LruCache。使用LruCache需要重写sizeOf方法,该方法的作用是计算缓存对象的大小,而且这里的大小的单位需要和总容量的单位一致,这里使用内存的1/8,单位是KB,sizeOf完成了Bitmap对象的大小计算。除了sizeOf方法,一些特殊情况下需要重写entryRemove方法,LruCache移除旧缓存时,会调用entryRemove方法,可以在该方法做一些回收工作。

该类要实现ImageCache接口,在实现的接口方法中实现设置缓存和获取缓存的工作即可,同时需要获取图片所对应的key,之所以将图片的url转成key,是因为图片url中可能有特殊字符,避免url在Android中的使用。

Bitmap工具类

BitmapUtils工具类

public class BitmapUtils
{/*** 获取缩放后的本地图片* @param res* @param resId* @param reqW* @param reqH* @return*/public static Bitmap decodeBitmapFormResource(Resources res, int resId, int reqW, int reqH){BitmapFactory.Options options = new BitmapFactory.Options();options.inJustDecodeBounds = true;BitmapFactory.decodeResource(res, resId, options);options.inSampleSize = calculateInSampleSize(options, reqW, reqH);options.inJustDecodeBounds = false;return BitmapFactory.decodeResource(res, resId, options);}/*** 根据需要的大小动态获取insampleSize的值** @param options* @param reqW* @param reqH* @return*/private static int calculateInSampleSize(BitmapFactory.Options options, int reqW, int reqH){int inSampleSize = 1;if(reqH == 0 || reqW == 0){return inSampleSize;}int outW = options.outWidth;int outH = options.outHeight;if(outW > reqW || outH > reqH){int halfW = outW / 2;int halfH = outH / 2;while((halfH / inSampleSize) >= reqH && (halfW / inSampleSize) >= reqW){inSampleSize *= 2;}}return inSampleSize;}/*** MD5* 将url转化成key* @param url* @return*/public static String hashKeyFormUrl(String url){String cacheKey;try{final MessageDigest mDigest = MessageDigest.getInstance("MD5");mDigest.update(url.getBytes());cacheKey = bytesToHexString(mDigest.digest());}catch(NoSuchAlgorithmException e){cacheKey = String.valueOf(url.hashCode());}return cacheKey;}private static String bytesToHexString(byte[] bytes){StringBuilder sb = new StringBuilder();for(int i = 0; i < bytes.length; i++){String hex = Integer.toHexString(0xFF & bytes[i]);if(hex.length() == 1){sb.append('0');}sb.append(hex);}return sb.toString();}/*** DiskLruCaceh* 获取缩放后的bitmap* @param fd* @param reqWidth* @param reqHeight* @return*/public static Bitmap decodeSampledBitmapFromFileDescriptor(FileDescriptor fd, int reqWidth, int reqHeight){// First decode with inJustDecodeBounds=true to check dimensionsfinal BitmapFactory.Options options = new BitmapFactory.Options();options.inJustDecodeBounds = true;BitmapFactory.decodeFileDescriptor(fd, null, options);// Calculate inSampleSizeoptions.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);// Decode bitmap with inSampleSize setoptions.inJustDecodeBounds = false;return BitmapFactory.decodeFileDescriptor(fd, null, options);}}
复制代码

注意:

1)hashKeyFormUrl方法是采用url的md5值作为key

2)decodeSampledBitmapFromFileDescriptor方法:由于FileInputStream是一种有序的文件流,而两次decodeStream调用会影响文件流的位置属性,导致第二次decodeStream时得到的是null,因此需要通过文件流来得到它所对应文件描述符,然后通过BitmapFactory.decodeFileDescriptor来加载一张缩放后的图片。

3)高效加载图片就是采用BitmapFactory.Options来加载所需要的尺寸图片,而Options来缩放图片,主要是通过inSampleSize参数,也就是采样率,方法比例为:

缩放比例 = 1/(inSampleSize的2的次方)

如一张ARGB8888的10241024图片原来大小为102410244,也就是4M,当inSampleSize = 2时,变为512512*4,也就是1M。

磁盘缓存实现

DiskLruCache是用于实现存储设备缓存,也就是磁盘缓存,它通过缓存对象写入文件系统从而实习缓存的效果。

但DiskLruCache并不属于Android SDK,其源码地址:

https://android.googlesource.com/platform/libcore/+/android-4.1.1_r1/luni/src/main/java/libcore/io/DiskLruCache.java

DiskCache :

/**** DiskLruCache磁盘缓存,它不属于Android sdk的一部分* 它的源码可以在这里下载 DiskLruCache的创建、缓存查找和缓存添加操作* https://android.googlesource.com/platform/libcore/+/android-4.1.1_r1/luni/src/main/java/libcore/io/DiskLruCache.java*/public class DiskCache implements ImageCache
{private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50;private DiskLruCache mDiskLruCache;//    static String cacheDir = "sdcard/myapplication/cache/";private boolean mIsDiskLruCacheCreated = false;private static final String TAG = "ImageLoader";private static final int DISK_CACHE_INDEX = 0;private static final int IO_BUFFER_SIZE = 8 * 1024;/*** 是否需要磁盘缓存* @return*/public boolean isDiskLruCacheCreated(){return mIsDiskLruCacheCreated;}/*** 设置是否需要磁盘缓存* @param diskLruCacheCreated*/public void setDiskLruCacheCreated(boolean diskLruCacheCreated){mIsDiskLruCacheCreated = diskLruCacheCreated;}public DiskCache(Context context){initCache(context);}/*** 初始化磁盘缓存* @param context*/private void initCache(Context context){File file = getDiskCacheDir(context,"myapplication/cache");if(!file.exists()){file.mkdirs();}try{mDiskLruCache = DiskLruCache.open(file, 1, 1, DISK_CACHE_SIZE);mIsDiskLruCacheCreated = true;}catch(IOException e){e.printStackTrace();}}private long getUsableSpace(File path){if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.GINGERBREAD){return path.getUsableSpace();}final StatFs stats = new StatFs(path.getPath());return (long)stats.getBlockSize() * (long)stats.getAvailableBlocks();}/*** 获取缓存路径* @param context* @param uniqueName* @return*/private File getDiskCacheDir(Context context, String uniqueName){
//      String path = getSdcardPath() + cacheDir;
//      return new File(path);boolean externalStorageAvailable = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);final String cachePath;if(externalStorageAvailable){cachePath = context.getExternalCacheDir().getPath();}else{cachePath = context.getCacheDir().getPath();}return new File(cachePath + File.separator + uniqueName);}/*** 获取sdcard路径* @return*/public static String getSdcardPath(){File sdcard = Environment.getExternalStorageDirectory();if(sdcard == null){return "";}return sdcard.getPath();}/*** 设置bitmap缓存* @param url* @param bitmap*/@Overridepublic void setCache(String url, Bitmap bitmap){String key = BitmapUtils.hashKeyFormUrl(url);try{DiskLruCache.Editor editor = mDiskLruCache.edit(key);if(editor != null){OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);if(downloadUrlToStream(url, outputStream)){editor.commit();}else{editor.abort();}mDiskLruCache.flush();}}catch(IOException e){e.printStackTrace();}}/*** 将图片下载写到磁盘** @param urlString* @param outputStream* @return*/private boolean downloadUrlToStream(String urlString, OutputStream outputStream){HttpURLConnection urlConnection = null;BufferedOutputStream out = null;BufferedInputStream in = null;try{final URL url = new URL(urlString);urlConnection = (HttpURLConnection)url.openConnection();in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE);out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE);int b;while((b = in.read()) != -1){out.write(b);}return true;}catch(IOException e){Log.e(TAG, "downloadBitmap failed." + e);}finally{if(urlConnection != null){urlConnection.disconnect();}MyUtils.close(out);MyUtils.close(in);}return false;}/*** 获取bitmap缓存* @param url* @param reqWidth* @param reqHeight* @return*/@Overridepublic Bitmap getCache(String url, int reqWidth, int reqHeight){Bitmap bitmap = null;String key = BitmapUtils.hashKeyFormUrl(url);try{if(!TextUtils.isEmpty(key)){DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);if(snapShot != null){FileInputStream fileInputStream = (FileInputStream)snapShot.getInputStream(DISK_CACHE_INDEX);FileDescriptor fileDescriptor = fileInputStream.getFD();bitmap = BitmapUtils.decodeSampledBitmapFromFileDescriptor(fileDescriptor, reqWidth, reqHeight);}}}catch(IOException e){e.printStackTrace();}return bitmap;}}
复制代码

open(File directory, int appVersion, int valueCount, long maxSize): directory:磁盘缓存在文件系统中的存储路径。缓存路径可以选sdcard的缓存路径(/sdcard/Android/data/pachkage_name/cache)目录,此目录应用卸载会被删除,当然也可以指定其他目录。 appVersion:版本号,一般设置1即可 valueCount:单个节点所对应的数据个数,一般设置1即可 maxSize:缓存总大小

downloadUrlToStream方法:该方法并没有真正的将图片写入文件系统,还需要通过setCache方法的Editor的commit方法来提交写入操作,如果图片下载过程发生了异常,还可以通过Editor的abort方法回退整个操作。

该类同样需要实现ImageCache来实现设置缓存和获取缓存。

DiskLruCache源码
/** Copyright (C) 2011 The Android Open Source Project** Licensed under the Apache License, Version 2.0 (the "License");* you may not use this file except in compliance with the License.* You may obtain a copy of the License at**      http://www.apache.org/licenses/LICENSE-2.0** Unless required by applicable law or agreed to in writing, software* distributed under the License is distributed on an "AS IS" BASIS,* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.* See the License for the specific language governing permissions and* limitations under the License.*/import java.io.BufferedInputStream;
import java.io.BufferedWriter;
import java.io.Closeable;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.reflect.Array;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;/*** ****************************************************************************** Taken from the JB source code, can be found in:* libcore/luni/src/main/java/libcore/io/DiskLruCache.java* or direct link:* https://android.googlesource.com/platform/libcore/+/android-4.1.1_r1/luni/src/main/java/libcore/io/DiskLruCache.java* ****************************************************************************** <p>* A cache that uses a bounded amount of space on a filesystem. Each cache* entry has a string key and a fixed number of values. Values are byte* sequences, accessible as streams or files. Each value must be between {@code* 0} and {@code Integer.MAX_VALUE} bytes in length.* <p>* <p>The cache stores its data in a directory on the filesystem. This* directory must be exclusive to the cache; the cache may delete or overwrite* files from its directory. It is an error for multiple processes to use the* same cache directory at the same time.* <p>* <p>This cache limits the number of bytes that it will store on the* filesystem. When the number of stored bytes exceeds the limit, the cache will* remove entries in the background until the limit is satisfied. The limit is* not strict: the cache may temporarily exceed it while waiting for files to be* deleted. The limit does not include filesystem overhead or the cache* journal so space-sensitive applications should set a conservative limit.* <p>* <p>Clients call {@link #edit} to create or update the values of an entry. An* entry may have only one editor at one time; if a value is not available to be* edited then {@link #edit} will return null.* <ul>* <li>When an entry is being <strong>created</strong> it is necessary to* supply a full set of values; the empty value should be used as a* placeholder if necessary.* <li>When an entry is being <strong>edited</strong>, it is not necessary* to supply data for every value; values default to their previous* value.* </ul>* Every {@link #edit} call must be matched by a call to {@link Editor#commit}* or {@link Editor#abort}. Committing is atomic: a read observes the full set* of values as they were before or after the commit, but never a mix of values.* <p>* <p>Clients call {@link #get} to read a snapshot of an entry. The read will* observe the value at the time that {@link #get} was called. Updates and* removals after the call do not impact ongoing reads.* <p>* <p>This class is tolerant of some I/O errors. If files are missing from the* filesystem, the corresponding entries will be dropped from the cache. If* an error occurs while writing a cache value, the edit will fail silently.* Callers should handle other problems by catching {@code IOException} and* responding appropriately.*  磁盘缓存*/
public final class DiskLruCache implements Closeable
{static final String JOURNAL_FILE = "journal";static final String JOURNAL_FILE_TMP = "journal.tmp";static final String MAGIC = "libcore.io.DiskLruCache";static final String VERSION_1 = "1";static final long ANY_SEQUENCE_NUMBER = -1;private static final String CLEAN = "CLEAN";private static final String DIRTY = "DIRTY";private static final String REMOVE = "REMOVE";private static final String READ = "READ";private static final Charset UTF_8 = Charset.forName("UTF-8");private static final int IO_BUFFER_SIZE = 8 * 1024;/** This cache uses a journal file named "journal". A typical journal file* looks like this:*     libcore.io.DiskLruCache*     1*     100*     2**     CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054*     DIRTY 335c4c6028171cfddfbaae1a9c313c52*     CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342*     REMOVE 335c4c6028171cfddfbaae1a9c313c52*     DIRTY 1ab96a171faeeee38496d8b330771a7a*     CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234*     READ 335c4c6028171cfddfbaae1a9c313c52*     READ 3400330d1dfc7f3f7f4b8d4d803dfcf6** The first five lines of the journal form its header. They are the* constant string "libcore.io.DiskLruCache", the disk cache's version,* the application's version, the value count, and a blank line.** Each of the subsequent lines in the file is a record of the state of a* cache entry. Each line contains space-separated values: a state, a key,* and optional state-specific values.*   o DIRTY lines track that an entry is actively being created or updated.*     Every successful DIRTY action should be followed by a CLEAN or REMOVE*     action. DIRTY lines without a matching CLEAN or REMOVE indicate that*     temporary files may need to be deleted.*   o CLEAN lines track a cache entry that has been successfully published*     and may be read. A publish line is followed by the lengths of each of*     its values.*   o READ lines track accesses for LRU.*   o REMOVE lines track entries that have been deleted.** The journal file is appended to as cache operations occur. The journal may* occasionally be compacted by dropping redundant lines. A temporary file named* "journal.tmp" will be used during compaction; that file should be deleted if* it exists when the cache is opened.*/private final File directory;private final File journalFile;private final File journalFileTmp;private final int appVersion;private final long maxSize;private final int valueCount;private long size = 0;private Writer journalWriter;private final LinkedHashMap<String, Entry> lruEntries = new LinkedHashMap<String, Entry>(0, 0.75f, true);private int redundantOpCount;/*** To differentiate between old and current snapshots, each entry is given* a sequence number each time an edit is committed. A snapshot is stale if* its sequence number is not equal to its entry's sequence number.*/private long nextSequenceNumber = 0;/* From java.util.Arrays */@SuppressWarnings("unchecked")private static <T> T[] copyOfRange(T[] original, int start, int end){final int originalLength = original.length; // For exception priority compatibility.if(start > end){throw new IllegalArgumentException();}if(start < 0 || start > originalLength){throw new ArrayIndexOutOfBoundsException();}final int resultLength = end - start;final int copyLength = Math.min(resultLength, originalLength - start);final T[] result = (T[])Array.newInstance(original.getClass().getComponentType(), resultLength);System.arraycopy(original, start, result, 0, copyLength);return result;}/*** Returns the remainder of 'reader' as a string, closing it when done.*/public static String readFully(Reader reader) throws IOException{try{StringWriter writer = new StringWriter();char[] buffer = new char[1024];int count;while((count = reader.read(buffer)) != -1){writer.write(buffer, 0, count);}return writer.toString();}finally{reader.close();}}/*** Returns the ASCII characters up to but not including the next "\r\n", or* "\n".** @throws EOFException if the stream is exhausted before the next newline*                              character.*/public static String readAsciiLine(InputStream in) throws IOException{// TODO: support UTF-8 here insteadStringBuilder result = new StringBuilder(80);while(true){int c = in.read();if(c == -1){throw new EOFException();}else if(c == '\n'){break;}result.append((char)c);}int length = result.length();if(length > 0 && result.charAt(length - 1) == '\r'){result.setLength(length - 1);}return result.toString();}/*** Closes 'closeable', ignoring any checked exceptions. Does nothing if 'closeable' is null.*/public static void closeQuietly(Closeable closeable){if(closeable != null){try{closeable.close();}catch(RuntimeException rethrown){throw rethrown;}catch(Exception ignored){}}}/*** Recursively delete everything in {@code dir}.*/// TODO: this should specify paths as Strings rather than as Filespublic static void deleteContents(File dir) throws IOException{File[] files = dir.listFiles();if(files == null){throw new IllegalArgumentException("not a directory: " + dir);}for(File file : files){if(file.isDirectory()){deleteContents(file);}if(!file.delete()){throw new IOException("failed to delete file: " + file);}}}/*** This cache uses a single background thread to evict entries.*/private final ExecutorService executorService = new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());private final Callable<Void> cleanupCallable = new Callable<Void>(){@Overridepublic Void call() throws Exception{synchronized(DiskLruCache.this){if(journalWriter == null){return null; // closed}trimToSize();if(journalRebuildRequired()){rebuildJournal();redundantOpCount = 0;}}return null;}};private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize){this.directory = directory;this.appVersion = appVersion;this.journalFile = new File(directory, JOURNAL_FILE);this.journalFileTmp = new File(directory, JOURNAL_FILE_TMP);this.valueCount = valueCount;this.maxSize = maxSize;}/*** Opens the cache in {@code directory}, creating a cache if none exists* there.** @param directory  a writable directory* @param appVersion* @param valueCount the number of values per cache entry. Must be positive.* @param maxSize    the maximum number of bytes this cache should use to store* @throws IOException if reading or writing the cache directory fails*/public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize) throws IOException{if(maxSize <= 0){throw new IllegalArgumentException("maxSize <= 0");}if(valueCount <= 0){throw new IllegalArgumentException("valueCount <= 0");}// prefer to pick up where we left offDiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);if(cache.journalFile.exists()){try{cache.readJournal();cache.processJournal();cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true), IO_BUFFER_SIZE);return cache;}catch(IOException journalIsCorrupt){
//                System.logW("DiskLruCache " + directory + " is corrupt: "
//                        + journalIsCorrupt.getMessage() + ", removing");cache.delete();}}// create a new empty cachedirectory.mkdirs();cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);cache.rebuildJournal();return cache;}private void readJournal() throws IOException{InputStream in = new BufferedInputStream(new FileInputStream(journalFile), IO_BUFFER_SIZE);try{String magic = readAsciiLine(in);String version = readAsciiLine(in);String appVersionString = readAsciiLine(in);String valueCountString = readAsciiLine(in);String blank = readAsciiLine(in);if(!MAGIC.equals(magic) || !VERSION_1.equals(version) || !Integer.toString(appVersion).equals(appVersionString) || !Integer.toString(valueCount).equals(valueCountString) || !"".equals(blank)){throw new IOException("unexpected journal header: [" + magic + ", " + version + ", " + valueCountString + ", " + blank + "]");}while(true){try{readJournalLine(readAsciiLine(in));}catch(EOFException endOfJournal){break;}}}finally{closeQuietly(in);}}private void readJournalLine(String line) throws IOException{String[] parts = line.split(" ");if(parts.length < 2){throw new IOException("unexpected journal line: " + line);}String key = parts[1];if(parts[0].equals(REMOVE) && parts.length == 2){lruEntries.remove(key);return;}Entry entry = lruEntries.get(key);if(entry == null){entry = new Entry(key);lruEntries.put(key, entry);}if(parts[0].equals(CLEAN) && parts.length == 2 + valueCount){entry.readable = true;entry.currentEditor = null;entry.setLengths(copyOfRange(parts, 2, parts.length));}else if(parts[0].equals(DIRTY) && parts.length == 2){entry.currentEditor = new Editor(entry);}else if(parts[0].equals(READ) && parts.length == 2){// this work was already done by calling lruEntries.get()}else{throw new IOException("unexpected journal line: " + line);}}/*** Computes the initial size and collects garbage as a part of opening the* cache. Dirty entries are assumed to be inconsistent and will be deleted.*/private void processJournal() throws IOException{deleteIfExists(journalFileTmp);for(Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ){Entry entry = i.next();if(entry.currentEditor == null){for(int t = 0; t < valueCount; t++){size += entry.lengths[t];}}else{entry.currentEditor = null;for(int t = 0; t < valueCount; t++){deleteIfExists(entry.getCleanFile(t));deleteIfExists(entry.getDirtyFile(t));}i.remove();}}}/*** Creates a new journal that omits redundant information. This replaces the* current journal if it exists.*/private synchronized void rebuildJournal() throws IOException{if(journalWriter != null){journalWriter.close();}Writer writer = new BufferedWriter(new FileWriter(journalFileTmp), IO_BUFFER_SIZE);writer.write(MAGIC);writer.write("\n");writer.write(VERSION_1);writer.write("\n");writer.write(Integer.toString(appVersion));writer.write("\n");writer.write(Integer.toString(valueCount));writer.write("\n");writer.write("\n");for(Entry entry : lruEntries.values()){if(entry.currentEditor != null){writer.write(DIRTY + ' ' + entry.key + '\n');}else{writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');}}writer.close();journalFileTmp.renameTo(journalFile);journalWriter = new BufferedWriter(new FileWriter(journalFile, true), IO_BUFFER_SIZE);}private static void deleteIfExists(File file) throws IOException{
//        try {
//            Libcore.os.remove(file.getPath());
//        } catch (ErrnoException errnoException) {
//            if (errnoException.errno != OsConstants.ENOENT) {
//                throw errnoException.rethrowAsIOException();
//            }
//        }if(file.exists() && !file.delete()){throw new IOException();}}/*** Returns a snapshot of the entry named {@code key}, or null if it doesn't* exist is not currently readable. If a value is returned, it is moved to* the head of the LRU queue.*/public synchronized Snapshot get(String key) throws IOException{checkNotClosed();validateKey(key);Entry entry = lruEntries.get(key);if(entry == null){return null;}if(!entry.readable){return null;}/** Open all streams eagerly to guarantee that we see a single published* snapshot. If we opened streams lazily then the streams could come* from different edits.*/InputStream[] ins = new InputStream[valueCount];try{for(int i = 0; i < valueCount; i++){ins[i] = new FileInputStream(entry.getCleanFile(i));}}catch(FileNotFoundException e){// a file must have been deleted manually!return null;}redundantOpCount++;journalWriter.append(READ + ' ' + key + '\n');if(journalRebuildRequired()){executorService.submit(cleanupCallable);}return new Snapshot(key, entry.sequenceNumber, ins);}/*** Returns an editor for the entry named {@code key}, or null if another* edit is in progress.*/public Editor edit(String key) throws IOException{return edit(key, ANY_SEQUENCE_NUMBER);}private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException{checkNotClosed();validateKey(key);Entry entry = lruEntries.get(key);if(expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null || entry.sequenceNumber != expectedSequenceNumber)){return null; // snapshot is stale}if(entry == null){entry = new Entry(key);lruEntries.put(key, entry);}else if(entry.currentEditor != null){return null; // another edit is in progress}Editor editor = new Editor(entry);entry.currentEditor = editor;// flush the journal before creating files to prevent file leaksjournalWriter.write(DIRTY + ' ' + key + '\n');journalWriter.flush();return editor;}/*** Returns the directory where this cache stores its data.*/public File getDirectory(){return directory;}/*** Returns the maximum number of bytes that this cache should use to store* its data.*/public long maxSize(){return maxSize;}/*** Returns the number of bytes currently being used to store the values in* this cache. This may be greater than the max size if a background* deletion is pending.*/public synchronized long size(){return size;}private synchronized void completeEdit(Editor editor, boolean success) throws IOException{Entry entry = editor.entry;if(entry.currentEditor != editor){throw new IllegalStateException();}// if this edit is creating the entry for the first time, every index must have a valueif(success && !entry.readable){for(int i = 0; i < valueCount; i++){if(!entry.getDirtyFile(i).exists()){editor.abort();throw new IllegalStateException("edit didn't create file " + i);}}}for(int i = 0; i < valueCount; i++){File dirty = entry.getDirtyFile(i);if(success){if(dirty.exists()){File clean = entry.getCleanFile(i);dirty.renameTo(clean);long oldLength = entry.lengths[i];long newLength = clean.length();entry.lengths[i] = newLength;size = size - oldLength + newLength;}}else{deleteIfExists(dirty);}}redundantOpCount++;entry.currentEditor = null;if(entry.readable | success){entry.readable = true;journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');if(success){entry.sequenceNumber = nextSequenceNumber++;}}else{lruEntries.remove(entry.key);journalWriter.write(REMOVE + ' ' + entry.key + '\n');}if(size > maxSize || journalRebuildRequired()){executorService.submit(cleanupCallable);}}/*** We only rebuild the journal when it will halve the size of the journal* and eliminate at least 2000 ops.*/private boolean journalRebuildRequired(){final int REDUNDANT_OP_COMPACT_THRESHOLD = 2000;return redundantOpCount >= REDUNDANT_OP_COMPACT_THRESHOLD && redundantOpCount >= lruEntries.size();}/*** Drops the entry for {@code key} if it exists and can be removed. Entries* actively being edited cannot be removed.** @return true if an entry was removed.*/public synchronized boolean remove(String key) throws IOException{checkNotClosed();validateKey(key);Entry entry = lruEntries.get(key);if(entry == null || entry.currentEditor != null){return false;}for(int i = 0; i < valueCount; i++){File file = entry.getCleanFile(i);if(!file.delete()){throw new IOException("failed to delete " + file);}size -= entry.lengths[i];entry.lengths[i] = 0;}redundantOpCount++;journalWriter.append(REMOVE + ' ' + key + '\n');lruEntries.remove(key);if(journalRebuildRequired()){executorService.submit(cleanupCallable);}return true;}/*** Returns true if this cache has been closed.*/public boolean isClosed(){return journalWriter == null;}private void checkNotClosed(){if(journalWriter == null){throw new IllegalStateException("cache is closed");}}/*** Force buffered operations to the filesystem.*/public synchronized void flush() throws IOException{checkNotClosed();trimToSize();journalWriter.flush();}/*** Closes this cache. Stored values will remain on the filesystem.*/public synchronized void close() throws IOException{if(journalWriter == null){return; // already closed}for(Entry entry : new ArrayList<Entry>(lruEntries.values())){if(entry.currentEditor != null){entry.currentEditor.abort();}}trimToSize();journalWriter.close();journalWriter = null;}private void trimToSize() throws IOException{while(size > maxSize){
//            Map.Entry<String, Entry> toEvict = lruEntries.eldest();final Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();remove(toEvict.getKey());}}/*** Closes the cache and deletes all of its stored values. This will delete* all files in the cache directory including files that weren't created by* the cache.*/public void delete() throws IOException{close();deleteContents(directory);}private void validateKey(String key){if(key.contains(" ") || key.contains("\n") || key.contains("\r")){throw new IllegalArgumentException("keys must not contain spaces or newlines: \"" + key + "\"");}}private static String inputStreamToString(InputStream in) throws IOException{return readFully(new InputStreamReader(in, UTF_8));}/*** A snapshot of the values for an entry.*/public final class Snapshot implements Closeable{private final String key;private final long sequenceNumber;private final InputStream[] ins;private Snapshot(String key, long sequenceNumber, InputStream[] ins){this.key = key;this.sequenceNumber = sequenceNumber;this.ins = ins;}/*** Returns an editor for this snapshot's entry, or null if either the* entry has changed since this snapshot was created or if another edit* is in progress.*/public Editor edit() throws IOException{return DiskLruCache.this.edit(key, sequenceNumber);}/*** Returns the unbuffered stream with the value for {@code index}.*/public InputStream getInputStream(int index){return ins[index];}/*** Returns the string value for {@code index}.*/public String getString(int index) throws IOException{return inputStreamToString(getInputStream(index));}@Overridepublic void close(){for(InputStream in : ins){closeQuietly(in);}}}/*** Edits the values for an entry.*/public final class Editor{private final Entry entry;private boolean hasErrors;private Editor(Entry entry){this.entry = entry;}/*** Returns an unbuffered input stream to read the last committed value,* or null if no value has been committed.*/public InputStream newInputStream(int index) throws IOException{synchronized(DiskLruCache.this){if(entry.currentEditor != this){throw new IllegalStateException();}if(!entry.readable){return null;}return new FileInputStream(entry.getCleanFile(index));}}/*** Returns the last committed value as a string, or null if no value* has been committed.*/public String getString(int index) throws IOException{InputStream in = newInputStream(index);return in != null ? inputStreamToString(in) : null;}/*** Returns a new unbuffered output stream to write the value at* {@code index}. If the underlying output stream encounters errors* when writing to the filesystem, this edit will be aborted when* {@link #commit} is called. The returned output stream does not throw* IOExceptions.*/public OutputStream newOutputStream(int index) throws IOException{synchronized(DiskLruCache.this){if(entry.currentEditor != this){throw new IllegalStateException();}return new FaultHidingOutputStream(new FileOutputStream(entry.getDirtyFile(index)));}}/*** Sets the value at {@code index} to {@code value}.*/public void set(int index, String value) throws IOException{Writer writer = null;try{writer = new OutputStreamWriter(newOutputStream(index), UTF_8);writer.write(value);}finally{closeQuietly(writer);}}/*** Commits this edit so it is visible to readers.  This releases the* edit lock so another edit may be started on the same key.*/public void commit() throws IOException{if(hasErrors){completeEdit(this, false);remove(entry.key); // the previous entry is stale}else{completeEdit(this, true);}}/*** Aborts this edit. This releases the edit lock so another edit may be* started on the same key.*/public void abort() throws IOException{completeEdit(this, false);}private class FaultHidingOutputStream extends FilterOutputStream{private FaultHidingOutputStream(OutputStream out){super(out);}@Overridepublic void write(int oneByte){try{out.write(oneByte);}catch(IOException e){hasErrors = true;}}@Overridepublic void write(byte[] buffer, int offset, int length){try{out.write(buffer, offset, length);}catch(IOException e){hasErrors = true;}}@Overridepublic void close(){try{out.close();}catch(IOException e){hasErrors = true;}}@Overridepublic void flush(){try{out.flush();}catch(IOException e){hasErrors = true;}}}}private final class Entry{private final String key;/*** Lengths of this entry's files.*/private final long[] lengths;/*** True if this entry has ever been published*/private boolean readable;/*** The ongoing edit or null if this entry is not being edited.*/private Editor currentEditor;/*** The sequence number of the most recently committed edit to this entry.*/private long sequenceNumber;private Entry(String key){this.key = key;this.lengths = new long[valueCount];}public String getLengths() throws IOException{StringBuilder result = new StringBuilder();for(long size : lengths){result.append(' ').append(size);}return result.toString();}/*** Set lengths using decimal numbers like "10123".*/private void setLengths(String[] strings) throws IOException{if(strings.length != valueCount){throw invalidLengths(strings);}try{for(int i = 0; i < strings.length; i++){lengths[i] = Long.parseLong(strings[i]);}}catch(NumberFormatException e){throw invalidLengths(strings);}}private IOException invalidLengths(String[] strings) throws IOException{throw new IOException("unexpected journal line: " + Arrays.toString(strings));}public File getCleanFile(int i){return new File(directory, key + "." + i);}public File getDirtyFile(int i){return new File(directory, key + "." + i + ".tmp");}}
}
复制代码
关闭流工具
public class MyUtils
{/*** 关闭流* @param out*/public static void close(Closeable out){if(out != null){try{out.close();}catch(IOException e){e.printStackTrace();}}}
}
复制代码

该工具中的close方法可以关闭任何流,因为流都实现了Closeable接口的close方法。

ImageLoader实现

ImageLoader:

public class ImageLoader
{private static final String TAG = "ImageLoader";private static final int TAG_KEY_URI = R.id.gridview_item_imageview;private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();private static final int CORE_POOL_SIZE = CPU_COUNT + 1;private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;private static final long KEEP_ALIVE = 10L;private Context mContext;private MemoryCache mMemoryCache;private DiskCache mDiskCache;public static final int MESSAGE_POST_RESULT = 1;private static final int IO_BUFFER_SIZE = 8 * 1024;private static final ThreadFactory sThreadFactory = new ThreadFactory(){private final AtomicInteger mCount = new AtomicInteger(1);public Thread newThread(Runnable r){return new Thread(r, "ImageLoader#" + mCount.getAndIncrement());}};/*** 线程池*/public static final Executor THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(), sThreadFactory);private Handler mMainHandler = new Handler(Looper.getMainLooper()){@Overridepublic void handleMessage(Message msg){LoaderResult result = (LoaderResult)msg.obj;ImageView imageView = result.imageView;String uri = (String)imageView.getTag(TAG_KEY_URI);if(uri.equals(result.uri)){imageView.setImageBitmap(result.bitmap);}else{Log.w(TAG, "set image bitmap,but url has changed, ignored!");}}};/*** 初始化对象* @param context*/public ImageLoader(Context context){mDiskCache = new DiskCache(context);mMemoryCache = new MemoryCache();this.mContext = context.getApplicationContext();}public static ImageLoader build(Context context){return new ImageLoader(context);}/*** 绑定bitmap** @param uri* @param imageView*/public void bindBitmap(String uri, ImageView imageView){bindBitmap(uri, imageView, 0, 0);}public void bindBitmap(final String uri, final ImageView imageView, final int reqW, final int reqH){imageView.setTag(TAG_KEY_URI, uri);Bitmap bitmap = mMemoryCache.getCache(uri, 0, 0);if(bitmap != null){imageView.setImageBitmap(bitmap);return;}Runnable runnable = new Runnable(){@Overridepublic void run(){Bitmap bitmap = loadBitmap(uri, reqW, reqH);if(bitmap != null){LoaderResult result = new LoaderResult(imageView, uri, bitmap);
//                  mMainHandler.obtainMessage(MESSAGE_POST_RESULT, result);Message msg = new Message();msg.what = MESSAGE_POST_RESULT;msg.obj = result;mMainHandler.sendMessage(msg);}}};THREAD_POOL_EXECUTOR.execute(runnable);}/*** 加载bitmap* @param uri* @param reqW* @param reqH* @return*/private Bitmap loadBitmap(String uri, int reqW, int reqH){Bitmap bitmap = mMemoryCache.getCache(uri, reqW, reqH);if(bitmap != null){return bitmap;}bitmap = mDiskCache.getCache(uri, reqH, reqH);if(bitmap != null){return bitmap;}bitmap = loadBitmapFromHttp(uri, reqW, reqH);return bitmap;}/*** 从网络获取bitmap* @param uri* @param reqW* @param reqH* @return*/private Bitmap loadBitmapFromHttp(String uri, int reqW, int reqH){Bitmap bitmap = null;if(Looper.myLooper() == Looper.getMainLooper()){throw new RuntimeException("can not visit network from UI Thread.");}if(mDiskCache == null){return null;}mDiskCache.setCache(uri, null);bitmap = mDiskCache.getCache(uri, reqW, reqH);if(bitmap != null){mMemoryCache.setCache(uri, bitmap);}if(bitmap == null && !mDiskCache.isDiskLruCacheCreated()){bitmap = downloadBitmapFromUrl(uri);}return bitmap;}/*** 加载网络图片* @param urlString* @return*/private Bitmap downloadBitmapFromUrl(String urlString){Bitmap bitmap = null;HttpURLConnection urlConnection = null;BufferedInputStream in = null;try{final URL url = new URL(urlString);urlConnection = (HttpURLConnection)url.openConnection();in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE);bitmap = BitmapFactory.decodeStream(in);}catch(final IOException e){Log.e(TAG, "Error in downloadBitmap: " + e);}finally{if(urlConnection != null){urlConnection.disconnect();}MyUtils.close(in);}return bitmap;}private static class LoaderResult{public ImageView imageView;public String uri;public Bitmap bitmap;public LoaderResult(ImageView imageView, String uri, Bitmap bitmap){this.imageView = imageView;this.uri = uri;this.bitmap = bitmap;}}
}
复制代码

注意: 1)采用线程池来实现网络获取,因为ImageLoader具有并发性,采用普通线程会产生大量的线程,影响性能,也不能采用AsyncTask,AsyncTask在3.0的低版本和高版本有差异。 2)获取Bitmap,先从内存获取,内存获取不到再从磁盘获取,最后磁盘获取不到才需要网络获取。 3)通过采用ImageView的setTag方法来解决图片错位的问题。

使用

ImageLoaderActivity:

public class ImageLoaderActivity extends AppCompatActivity
{private GridView mGridView;private List<String> mList = new ArrayList<>();private GridAdapter mGridAdapter;private Context mContext;private ImageLoader mImageLoader;public static final String[] PHOTOS = {"http://f.hiphotos.baidu.com/image/pic/item/faf2b2119313b07e97f760d908d7912396dd8c9c.jpg","http://g.hiphotos.baidu.com/image/pic/item/4b90f603738da977c76ab6fab451f8198718e39e.jpg","http://e.hiphotos.baidu.com/image/pic/item/902397dda144ad343de8b756d4a20cf430ad858f.jpg","http://a.hiphotos.baidu.com/image/pic/item/a6efce1b9d16fdfa0fbc1ebfb68f8c5495ee7b8b.jpg","http://b.hiphotos.baidu.com/image/pic/item/a71ea8d3fd1f4134e61e0f90211f95cad1c85e36.jpg","http://c.hiphotos.baidu.com/image/pic/item/7dd98d1001e939011b9c86d07fec54e737d19645.jpg","http://f.hiphotos.baidu.com/image/pic/item/f11f3a292df5e0fecc3e83ef586034a85edf723d.jpg","http://cdn.duitang.com/uploads/item/201309/17/20130917111400_CNmTr.thumb.224_0.png","http://pica.nipic.com/2007-10-17/20071017111345564_2.jpg","http://pic4.nipic.com/20091101/3672704_160309066949_2.jpg","http://pic4.nipic.com/20091203/1295091_123813163959_2.jpg","http://pic31.nipic.com/20130624/8821914_104949466000_2.jpg","http://pic6.nipic.com/20100330/4592428_113348099353_2.jpg","http://pic9.nipic.com/20100917/5653289_174356436608_2.jpg","http://img10.3lian.com/sc6/show02/38/65/386515.jpg","http://pic1.nipic.com/2008-12-09/200812910493588_2.jpg","http://pic2.ooopic.com/11/79/98/31bOOOPICb1_1024.jpg" };@Overrideprotected void onCreate(Bundle savedInstanceState){super.onCreate(savedInstanceState);setContentView(R.layout.activity_image_loader);this.mContext = ImageLoaderActivity.this;mImageLoader = ImageLoader.build(mContext);mGridView = (GridView)this.findViewById(R.id.gridlist);loadData();mGridAdapter = new GridAdapter();mGridView.setAdapter(mGridAdapter);}private void loadData(){for(int i = 0; i < 301; i++){String photo = PHOTOS[getRandomNum(PHOTOS.length)];mList.add(photo);}}public static int getRandomNum(int max) {Random random = new Random();int result = random.nextInt(max);return result;}private class GridAdapter extends BaseAdapter{@Overridepublic int getCount(){return mList.size();}@Overridepublic Object getItem(int position){return mList.get(position);}@Overridepublic long getItemId(int position){return position;}@Overridepublic View getView(int position, View convertView, ViewGroup parent){ImageView imageView;if(convertView == null){imageView=new ImageView(mContext);imageView.setBackgroundColor(0xffadadad);imageView.setLayoutParams(new GridView.LayoutParams(300, 300));imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);}else{imageView = (ImageView) convertView;}mImageLoader.bindBitmap(mList.get(position),imageView,300,300);return imageView;}}}
复制代码

使用的时候直接mImageLoader.bindBitmap(mList.get(position),imageView,300,300);方法即可。

布局文件:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayoutandroid:id="@+id/activity_image_loader"xmlns:android="http://schemas.android.com/apk/res/android"xmlns:tools="http://schemas.android.com/tools"android:layout_width="match_parent"android:layout_height="match_parent"android:paddingBottom="@dimen/activity_vertical_margin"android:paddingLeft="@dimen/activity_horizontal_margin"android:paddingRight="@dimen/activity_horizontal_margin"android:paddingTop="@dimen/activity_vertical_margin"tools:context="com.example.linwj.imageloader.ImageLoaderActivity"><GridViewandroid:id="@+id/gridlist"android:layout_width="match_parent"android:layout_height="match_parent"android:columnWidth="90dp"android:numColumns="auto_fit"android:horizontalSpacing="10dp"android:verticalSpacing="10dp"android:stretchMode="columnWidth"android:gravity="center"/></RelativeLayout>复制代码
效果图

上图效果就是本文打造的图片加载器的实战成果,该图片下载器采用了单一职责、开闭原则和接口分离原则等设计原则。

希望大家喜欢,如果读者有什么看法的可以给我留言。

参考文章: 《Android开发艺术探索》第十二章《Bitmap的加载和Cache》

Android中的缓存策略,实战打造ImageLoader相关推荐

  1. 【安卓中的缓存策略系列】安卓缓存策略之综合应用ImageLoader实现照片墙的效果

    在前面的[安卓缓存策略系列]安卓缓存之内存缓存LruCache和[安卓缓存策略系列]安卓缓存策略之磁盘缓存DiskLruCache这两篇博客中已经将安卓中的缓存策略的理论知识进行过详细讲解,还没看过这 ...

  2. Android中Bitmap缓存池

    http://mobile.51cto.com/android-386446.htm 本文介绍了如何使用缓存来提高UI的载入输入和滑动的流畅性.使用内存缓存.使用磁盘缓存.处理配置改变事件等方法将会有 ...

  3. Android中的缓存处理及异步加载图片类的封装

    一.缓存介绍: (一).Android中缓存的必要性: 智能手机的缓存管理应用非常的普遍和需要,是提高用户体验的有效手段之一. 1.没有缓存的弊端: 流量开销:对于客户端--服务器端应用,从远程获取图 ...

  4. Android中图片缓存、显示框架Glide的介绍与使用

    1 介绍 Glide是一个快速高效的Android图片加载库,注重于平滑的滚动.Glide提供了易用的API,高性能.可扩展的图片解码管道(decode pipeline),以及自动的资源池技术. G ...

  5. Android图片三级缓存策略

    1.简介 Android缓存原理都是一样,可以自己封装. 三级缓存: 1.内存缓存:缓存在内存中,基于LRU(least recently used )算法,机器重启消失.2.本地缓存.缓存在本地中. ...

  6. Android中获取缓存大小和清除缓存功能

    前言 本篇博客要给大家分享的如何获取应用缓存的大小和清除缓存的功能,我们知道我们应用当中经常会产生一些数据,比如图片的缓存,数据库文件,配置文件等等.我们开发产品的时候,可能会有这样的一个需求清除应用 ...

  7. Android中清除缓存

    有时候会遇到要清除应用缓存的功能,不经常用,总忘,所以在这里总结,实际价值个人感觉不大. /*** 缓存管理类*/ public class DataCleanManager {/*** 获取缓存大小 ...

  8. Connor学Android - Bitmap的加载和缓存策略

    Learn && Live 虚度年华浮萍于世,勤学善思至死不渝 前言 Hey,欢迎阅读Connor学Android系列,这个系列记录了我的Android原理知识学习.复盘过程,欢迎各位 ...

  9. Asp.Net中MVC缓存详解

    本文通过介绍了Asp.Net中MVC缓存的种类,以及他们之间的区别等内容,让学习者能够深入的了解MVC缓存的原理机制,以下是具体内容: 缓存是一种保存资源副本并在下次请求时直接使用该副本的技术.当 w ...

最新文章

  1. 百度认为什么样的网站更有抓取和收录价值
  2. 字符编码简介 ANSI Unicode Unicode big endian UTF-8
  3. EventBus-再也不用什么Handler了
  4. 达内出来的混得怎么样了_《士兵突击》主演现状:混得最好的不是王宝强,而是一向低调的他...
  5. 利用dsamain.exe挂载快照(活动目录快照配置管理系列四)
  6. 服务器内存一般多大_性能调优第一步,搞定服务器硬件选型
  7. mongodb中分页显示数据集的学习
  8. mysql g月份分组_PowerBI快捷键——视觉对象分组功能
  9. 多线程 调用 axis 报错_java笔记录(三、多线程)
  10. 找不到redis得pid文件_Pads 误删宏文件,打开后弹框报错找不到路径
  11. 【QT】入门基础教程Qt5
  12. C语言--输入一个数判断是否为素数(多种方法)
  13. python 高等数学实验_高等数学——基于Python的实现
  14. linux下的蓝牙驱动程序详解
  15. linux 如何停止mysql,linux下如何启动/停止/重启mysql:
  16. SAP采购定价过程-条件技术介绍
  17. 实时渲染3D动画创作大赛
  18. android edittext numberdecimal,android – 如何在“手机”软键盘上使用InputType = numberDecimal?...
  19. 平衡小车PID,就该这么调!!!
  20. 线程生命周期 什么时候会出现僵死进程

热门文章

  1. 如何运行matlab代码?怎样找到matlab主程序?
  2. kolla-ansible openstack登录 证书不可用
  3. AIOps 在腾讯的探索和实践
  4. 浏览器返回数据量过大报‘failed to load response data: Request content was evicted from inspector cache’
  5. Wear OS手表应用开发教程之-创建应用
  6. xshell如何将Windows文件上传到linux
  7. 表格布局管理器TableLayout
  8. 2022 智简魔方财务快云模板前台+购物车+用户中心模板 全解 无授权
  9. Web GIS多种方式发布动态地图服务及显示(2)
  10. 一个简单的例子解释什么是量子计算机