1. 程式人生 > >APP圖片快取與Glide之signature的分析

APP圖片快取與Glide之signature的分析

1.圖片快取遇到的問題

在快取網路圖片的過程中,有一種情況是圖片的地址不變,但圖片發生了變化,如果只按照圖片的地址進行快取,在載入快取中的圖片時就會發生圖片一直顯示為舊圖的現象。 在App中修改使用者頭像的功能中,如果伺服器儲存頭像的地址保持不變,載入快取就會出現上述的情況。馬上想到,當修改頭像之後,馬上將本地原頭像的快取清除,並載入新頭像,此方法只是暫時解決了APP端頭像的顯示效果,如果在其他終端進行了頭像修改,手機上並不能同步顯示最新圖片。 那麼該如何獲取最新的網路圖片呢?顯然不使用快取是肯定可以顯示最新的圖片,但要使用快取圖片功能,又希望可以獲取最新的圖片,我們需要記錄圖片是否發生了變化,根據變化與否,選擇是否更新快取中的內容。 有人說下載圖片之後,判斷其SHA值是否相同,即可得知圖片是否相同。然而,每次都要下載圖片,再判斷SHA值,還用快取做什麼,已經完全背道而馳。 好的做法是在伺服器上加上圖片是否改變的標識,在APP端儲存該值,在載入快取內容之前判斷是否有改變,需要更新快取內容。該標識可以使用時間戳,來記錄圖片更新時間,或使用累加數來記錄標識。 當然,以上說的是處理自己的伺服器上,資料可以增加欄位的情況。如果只是單純的載入網路上的圖片,可以在圖片下載之後,在APP中做標識,一段時間之內不更新,在一天或固定時間後檢測標識並更新網路圖片。比如一天更新一次,則可將日期作為標識。

2.Glide簡析

Glide作為一個優秀的載入圖片庫,提供了signature方法對圖片進行標識,現針對安卓Glide-3.7.0進行簡要分析。 Glide的一般呼叫方法為Glide.with(context).load(url).into(target);使用快取時.diskCacheStrategy(DiskCacheStrategy.ALL)來指定快取型別。 列舉類DiskCacheStrategy有兩個屬性,
private final boolean cacheSource;
private final boolean cacheResult;
//四個列舉值
/** Caches with both {@link #SOURCE} and {@link #RESULT}. */
ALL(true, true), //快取原檔案和處理後(如尺寸變化、型別轉換) 的資料

/** Saves no data to cache. */
NONE(false, false), //不快取

/** Saves just the original data to cache. */
SOURCE(true, false), //只快取原檔案

/** Saves the media item after all transformations to cache. */
RESULT(false, true); //只快取處理後的資料
也可以通過.signature(Key signature)方法傳入標識,來實現預期功能。傳入引數為實現Key介面的類物件,Glide中有三個類StringSignature,MediaStoreSignature,EmptySignature. 構造方法StringSignature(String signature)中傳入String,可以簡單傳入剛才說的時間戳進行標識。 構造方法MediaStoreSignature(String mimeType, long dateModified, intorientation)可以傳入圖片的mimeType,修改時間,圖片或視訊的方向 當然也可以自定義類實現Key介面並複寫其equals方法判斷標識相同,及updateDiskCacheKey方法去更新本地快取檔案。 現在說說Glide載入圖片的過程。 在into()方法(GenericRequestBuilder類中的方法)中建立了Request:
Request request = buildRequest(target);//建立GenericRequest例項
requestTracker.runRequest(request);//呼叫GenericRequest的begin方法
而在begin()方法中會呼叫onSizeReady(int width,int height)方法 此方法中呼叫engine.load方法開始載入圖片 Engine類中load方法部分如下:
final String id = fetcher.getId();//當載入url圖片時,id為圖片地址
EngineKey key = keyFactory.buildKey(id, signature, width, height, loadProvider.getCacheDecoder(),
     loadProvider.getSourceDecoder(), transformation, loadProvider.getEncoder(),
     transcoder, loadProvider.getSourceEncoder());//根據id,signature以及其他的資訊生成key,用於快取檔案的key

EngineResource<?> cached = loadFromCache(key, isMemoryCacheable);

EngineJob engineJob = engineJobFactory.build(key, isMemoryCacheable);

DecodeJob<T, Z, R> decodeJob = new DecodeJob<T, Z, R>(key, width, height, fetcher, loadProvider, transformation,

        transcoder, diskCacheProvider, diskCacheStrategy, priority);

EngineRunnable runnable = new EngineRunnable(engineJob, decodeJob, priority);

jobs.put(key, engineJob);

engineJob.addCallback(cb);
engineJob.start(runnable);

在runnable的run方法中呼叫decode()方法
private Resource<?> decode() throws Exception {

    if (isDecodingFromCache()) {

        return decodeFromCache();

    } else {

        return decodeFromSource();

    }
}

private boolean isDecodingFromCache() {

    return stage == Stage.CACHE;

}


而EngineRunnable的初始化中
this.stage = Stage.CACHE;
故首先執行decodeFromCache();首次載入返回空,呼叫:
private void onLoadFailed(Exception e) {
    if (isDecodingFromCache()) {
        stage = Stage.SOURCE;//改變值
        manager.submitForSource(this);//重新調起run()方法,之後進入decodeFromSource()方法
    } else {
        manager.onException(e);
    }

}

兩個方法長這樣:
private Resource<?> decodeFromCache() throws Exception {

    Resource<?> result = null;

    try {

        result = decodeJob.decodeResultFromCache();

    } catch (Exception e) {

        if (Log.isLoggable(TAG, Log.DEBUG)) {

            Log.d(TAG, "Exception decoding result from cache: " + e);

        }

    }

    if (result == null) {

        result = decodeJob.decodeSourceFromCache();

    }

    return result;
}

public Resource<Z> decodeResultFromCache() throws Exception {

    if (!diskCacheStrategy.cacheResult()) {
          //cacheResult ()判斷列舉類DiskCacheStrategy的型別,由初始化時的型別決定布林值,詳見文章開頭處
        return null;//如果沒有快取資料,返回null

    }

    long startTime = LogTime.getLogTime();

    Resource<T> transformed = loadFromCache(resultKey);//根據resultKey獲取transform後的資料

    if (Log.isLoggable(TAG, Log.VERBOSE)) {

        logWithTimeAndKey("Decoded transformed from cache", startTime);

    }

    startTime = LogTime.getLogTime();

    Resource<Z> result = transcode(transformed);

    if (Log.isLoggable(TAG, Log.VERBOSE)) {

        logWithTimeAndKey("Transcoded transformed from cache", startTime);

    }

    return result;
}
loadFromCache方法載入快取檔案:
private Resource<T> loadFromCache(Key key) throws IOException {

    File cacheFile = diskCacheProvider.getDiskCache().get(key);//根據key獲取快取檔案

    if (cacheFile == null) {

        return null;

    }

    Resource<T> result = null;

    try {

        result = loadProvider.getCacheDecoder().decode(cacheFile, width, height);//將檔案解碼為Resource

    } finally {

        if (result == null) {

            diskCacheProvider.getDiskCache().delete(key);//若Resource為空則key無效,刪除

        }

    }

    return result;
}
public Resource<Z> decodeSourceFromCache() throws Exception {

    if (!diskCacheStrategy.cacheSource()) {

        return null;

    }

    long startTime = LogTime.getLogTime();

    Resource<T> decoded = loadFromCache(resultKey.getOriginalKey());//根據OriginalKey獲取原檔案

    if (Log.isLoggable(TAG, Log.VERBOSE)) {

        logWithTimeAndKey("Decoded source from cache", startTime);

    }

    return transformEncodeAndTranscode(decoded);

}

OriginalKey的構造方法如下,只含有id和signature兩個屬性
public OriginalKey(String id, Key signature) {
    this.id = id;

    this.signature = signature;

}


上述decodeFromSource()的原始碼如下:
public Resource<Z> decodeFromSource() throws Exception {

    Resource<T> decoded = decodeSource();

    return transformEncodeAndTranscode(decoded);
}
decodeSource()方法最後調到了cacheAndDecodeSourceData方法:
private Resource<T> cacheAndDecodeSourceData(A data) throws IOException {
    long startTime = LogTime.getLogTime();

    SourceWriter<A> writer = new SourceWriter<A>(loadProvider.getSourceEncoder(), data);

    diskCacheProvider.getDiskCache().put(resultKey.getOriginalKey(), writer);//根據OriginalKey儲存圖片原檔案

    if (Log.isLoggable(TAG, Log.VERBOSE)) {

        logWithTimeAndKey("Wrote source to cache", startTime);

    }

    startTime = LogTime.getLogTime();

    Resource<T> result = loadFromCache(resultKey.getOriginalKey());

    if (Log.isLoggable(TAG, Log.VERBOSE) && result != null) {

        logWithTimeAndKey("Decoded source from cache", startTime);

    }

    return result;
}


也就是說,decodeFromCache()和 decodeFromSource()最後都呼叫了transformEncodeAndTranscode方法,將原檔案進行轉換transform:
private Resource<Z> transformEncodeAndTranscode(Resource<T> decoded) {

    long startTime = LogTime.getLogTime();

    Resource<T> transformed = transform(decoded);

    if (Log.isLoggable(TAG, Log.VERBOSE)) {

        logWithTimeAndKey("Transformed resource from source", startTime);

    }

    writeTransformedToCache(transformed);//會判斷是否需要快取轉換後的資料,根據diskCacheStrategy.cacheResult()結果決定

    startTime = LogTime.getLogTime();

    Resource<Z> result = transcode(transformed);

    if (Log.isLoggable(TAG, Log.VERBOSE)) {

        logWithTimeAndKey("Transcoded transformed from source", startTime);

    }

    return result;

}


如果diskCacheStrategy.cacheResult() 為true,此方法中的writeTransformedToCache方法會將資料快取起來:
diskCacheProvider.getDiskCache().put(resultKey, writer);//根據resultKey儲存transform之後的資料

這裡的diskCacheProvider又從哪裡來的呢?追蹤到Engine裡, this.diskCacheProvider = new LazyDiskCacheProvider(diskCacheFactory); 而此類中getDiskCache()方法如下:
public DiskCache getDiskCache() {
    if (diskCache == null) {

        synchronized (this) {

            if (diskCache == null) {

                diskCache = factory.build();

            }

            if (diskCache == null) {

                diskCache = new DiskCacheAdapter();

            }

        }

    }
    return diskCache;

}

繼續找factory發現,上面這些方法中的get,put方法都是介面方法,具體factory從Glide.with(context)時傳入: with方法context不同時,呼叫方法相同,以activity為例:
public static RequestManager with(Activity activity) {

    RequestManagerRetriever retriever = RequestManagerRetriever.get();

    return retriever.get(activity);
}


上述方法最終建立了RequestManager物件,在其構造方法中
this.glide = Glide.get(context);


get方法呼叫
glide = builder.createGlide();
GlideBuilder類中createGlide()方法有這麼幾行:
memoryCache = new LruResourceCache(calculator.getMemoryCacheSize());
diskCacheFactory = new InternalCacheDiskCacheFactory(context);
engine = new Engine(memoryCache, diskCacheFactory, diskCacheService, sourceService);
return new Glide(engine, memoryCache, bitmapPool, context, decodeFormat);

InternalCacheDiskCacheFactory 類繼承自DiskLruCacheFactory 這裡就是factory的build方法了
@Override

public DiskCache build() {

    File cacheDir = cacheDirectoryGetter.getCacheDirectory();

    if (cacheDir == null) {

        return null;

    }

    if (!cacheDir.mkdirs() && (!cacheDir.exists() || !cacheDir.isDirectory())) {

        return null;

    }

    return DiskLruCacheWrapper.get(cacheDir, diskCacheSize);
}


最後的get方法返回一個DiskLruCacheWrapper物件,也就是getDiskCache()返回的物件了,用來快取資料的方法都在這裡了:
public static synchronized DiskCache get(File directory, int maxSize) {

    // TODO calling twice with different arguments makes it return the cache for the same directory, it's public!

    if (wrapper == null) {

        wrapper = new DiskLruCacheWrapper(directory, maxSize);

    }

    return wrapper;
}


終於,上邊獲取快取檔案和儲存快取的方法都在DiskLruCacheWrapper類裡了:
private synchronized DiskLruCache getDiskCache() throws IOException {
    if (diskLruCache == null) {
        diskLruCache = DiskLruCache.open(directory, APP_VERSION, VALUE_COUNT, maxSize);//返回一個DiskLruCache物件
    }
    return diskLruCache;
}

@Override
public File get(Key key) {

    String safeKey = safeKeyGenerator.getSafeKey(key);

    File result = null;

    try {

        final DiskLruCache.Value value = getDiskCache().get(safeKey);

        if (value != null) {

            result = value.getFile(0);

        }

    } catch (IOException e) {

        if (Log.isLoggable(TAG, Log.WARN)) {

            Log.w(TAG, "Unable to get from disk cache", e);

        }

    }

    return result;

}

@Override

public void put(Key key, Writer writer) {

    String safeKey = safeKeyGenerator.getSafeKey(key);

    writeLocker.acquire(key);

    try {

        DiskLruCache.Editor editor = getDiskCache().edit(safeKey);

        // Editor will be null if there are two concurrent puts. In the worst case we will just silently fail.

        if (editor != null) {

            try {

                File file = editor.getFile(0);

                if (writer.write(file)) {

                    editor.commit();

                }

            } finally {

                editor.abortUnlessCommitted();

            }

        }

    } catch (IOException e) {

        if (Log.isLoggable(TAG, Log.WARN)) {

            Log.w(TAG, "Unable to put to disk cache", e);

        }

    } finally {

        writeLocker.release(key);

    }

}

@Override

public void delete(Key key) {

    String safeKey = safeKeyGenerator.getSafeKey(key);

    try {

        getDiskCache().remove(safeKey);

    } catch (IOException e) {

        if (Log.isLoggable(TAG, Log.WARN)) {

            Log.w(TAG, "Unable to delete from disk cache", e);

        }

    }

}

@Override

public synchronized void clear() {

    try {

        getDiskCache().delete();

        resetDiskCache();

    }  catch (IOException e) {

        if (Log.isLoggable(TAG, Log.WARN)) {

            Log.w(TAG, "Unable to clear disk cache", e);

        }

    }
}


這些get,put的方法又是呼叫的DiskLruCache中的get,put方法,方法返回Value物件 其中用到了Entry類來儲存檔案file, Entry的構造方法如下:
private Entry(String key) {

  this.key = key;

  this.lengths = new long[valueCount];

  cleanFiles = new File[valueCount];

  dirtyFiles = new File[valueCount];

  // The names are repetitive so re-use the same builder to avoid allocations.

  StringBuilder fileBuilder = new StringBuilder(key).append('.');

  int truncateTo = fileBuilder.length();

  for (int i = 0; i < valueCount; i++) {

      fileBuilder.append(i);

      cleanFiles[i] = new File(directory, fileBuilder.toString());

      fileBuilder.append(".tmp");

      dirtyFiles[i] = new File(directory, fileBuilder.toString());

      fileBuilder.setLength(truncateTo);

  }

}


get/put方法中,生成key的方法如下:
class SafeKeyGenerator {

    private final LruCache<Key, String> loadIdToSafeHash = new LruCache<Key, String>(1000);

    public String getSafeKey(Key key) {

        String safeKey;

        synchronized (loadIdToSafeHash) {

            safeKey = loadIdToSafeHash.get(key);

        }

        if (safeKey == null) {

            try {

                MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");

                key.updateDiskCacheKey(messageDigest);

                safeKey = Util.sha256BytesToHex(messageDigest.digest());

            } catch (UnsupportedEncodingException e) {

                e.printStackTrace();

            } catch (NoSuchAlgorithmException e) {

                e.printStackTrace();

            }

            synchronized (loadIdToSafeHash) {

                loadIdToSafeHash.put(key, safeKey);

            }

        }

        return safeKey;

    }
}


其中,LruCache用了一個Map儲存key及其對應的加密後的字串, LruCache的程式碼不多,都放上吧:
public class LruCache<T, Y> {

    private final LinkedHashMap<T, Y> cache = new LinkedHashMap<T, Y>(100, 0.75f, true);//最後的最後,這裡是用一個LinkedHashMap對key和快取進行了儲存。

    private int maxSize;

    private final int initialMaxSize;

    private int currentSize = 0;

    public LruCache(int size) {

        this.initialMaxSize = size;

        this.maxSize = size;

    }

    public void setSizeMultiplier(float multiplier) {

        if (multiplier < 0) {

            throw new IllegalArgumentException("Multiplier must be >= 0");

        }

        maxSize = Math.round(initialMaxSize * multiplier);

        evict();

    }

    protected int getSize(Y item) {

        return 1;

    }

    protected void onItemEvicted(T key, Y item) {

        // optional override

    }

    public int getMaxSize() {

        return maxSize;

    }

    public int getCurrentSize() {

        return currentSize;

    }

    public boolean contains(T key) {

        return cache.containsKey(key);

    }

    public Y get(T key) {

        return cache.get(key);

    }

    public Y put(T key, Y item) {

        final int itemSize = getSize(item);

        if (itemSize >= maxSize) {

            onItemEvicted(key, item);

            return null;

        }

        final Y result = cache.put(key, item);

        if (item != null) {

            currentSize += getSize(item);

        }

        if (result != null) {

            // TODO: should we call onItemEvicted here?

            currentSize -= getSize(result);

        }

        evict();

        return result;

    }

    public Y remove(T key) {

        final Y value = cache.remove(key);

        if (value != null) {

            currentSize -= getSize(value);

        }

        return value;

    }

    public void clearMemory() {

        trimToSize(0);

    }

    protected void trimToSize(int size) {

        Map.Entry<T, Y> last;

        while (currentSize > size) {

            last = cache.entrySet().iterator().next();

            final Y toRemove = last.getValue();

            currentSize -= getSize(toRemove);

            final T key = last.getKey();

            cache.remove(key);

            onItemEvicted(key, toRemove);

        }

    }

    private void evict() {

        trimToSize(maxSize);

    }
}


另外:
public static String sha1BytesToHex(byte[] bytes) {

    synchronized (SHA_1_CHARS) {

        return bytesToHex(bytes, SHA_1_CHARS);

    }

}

private static String bytesToHex(byte[] bytes, char[] hexChars) {

    int v;

    for (int j = 0; j < bytes.length; j++) {

        v = bytes[j] & 0xFF;

        hexChars[j * 2] = HEX_CHAR_ARRAY[v >>> 4];

        hexChars[j * 2 + 1] = HEX_CHAR_ARRAY[v & 0x0F];

    }

    return new String(hexChars);
}

總結: Glide的signature方法有效地添加了標識,通過簡單的傳入StringSignature即可實現判斷圖片資訊是否為最新,從而載入最新的圖片。 在自行開發過程中,可以更好的處理舊圖片問題,把標識資訊和圖片分開存放,如有圖片更新,可刪除舊圖片,只快取新圖片。當然,主要看是否需要儲存舊圖片。 P.S.僅對Glide一部分功能進行了簡要分析,有興趣的讀者可以參見https://github.com/bumptech/glide進行進一步研究,如上述內容有紕漏,多謝指正。

相關推薦

APP圖片快取Glidesignature分析

1.圖片快取遇到的問題 在快取網路圖片的過程中,有一種情況是圖片的地址不變,但圖片發生了變化,如果只按照圖片的地址進行快取,在載入快取中的圖片時就會發生圖片一直顯示為舊圖的現象。 在App中修改使用者頭像的功能中,如果伺服器儲存頭像的地址保持不變,載入快取就會出現上述的情

Android圖片載入框架Glide探究Glide快取機制

轉載自:http://blog.csdn.net/guolin_blog/article/details/54895665 在本系列的上一篇文章中,我帶著大家一起閱讀了一遍Glide的原始碼,初步瞭解了這個強大的圖片載入框架的基本執行流程。 不過,上一篇文

網站運維技術實踐資料分析報警

  對於日益積累的監控資料,顯然需要有規劃地進行儲存和分析,做到“故障沒來時有預防,故障來臨時有提示,故障到來時有解決方案”。      一、時間序列儲存      對於大多數監控資料,都有一個天然的類似資料庫主鍵的屬性,那就是時間。所以,通常情況下,各類監控系統的後臺資料庫都可以認為是時間序列的資

Android實現圖片快取非同步載入

ImageManager2這個類具有非同步從網路下載圖片,從sd讀取本地圖片,記憶體快取,硬碟快取,圖片使用動畫漸現等功能,已經將其應用在包含大量圖片的應用中一年多,沒有出現oom。 Android程式常常會記憶體溢位,網上也有很多解決方案,如軟引用,手動呼叫recycle

Android開發圖片快取框架Glide的總結

前言 前段時間寫過一篇圖片快取框架Picasso的用法,對於Picasso有些同學也比較熟悉,採用Lru最近最少快取策略,並且自帶記憶體和硬碟快取機制,在圖片載入尤其是多圖載入著實為大夥省了不少力,在此之前同樣也相識有Afinal、Xutil、Univer

Android 圖片快取載入方式

開場白 從開始開發Android到現在使用的好多載入圖片的框架,剛開始什麼都不懂就看第三方封裝的框架是如何載入的,然後照搬過來使用,只要能加載出圖片就算工作完成,我才不考慮什麼好不好?對不對?因為我自

ImageLoader圖片快取簡單配置詳細配置

ImageLoader依賴 implementation 'com.nostra13.universalimageloader:universal-image-loader:1.9.5' 1.簡單配置 public class App extends Applicati

Glide圖片載入神器----官方庫wiki說明文件的翻譯(快取快取校驗)

快取校驗是一個相對複雜的話題和概念,應該儘量少去考慮。本篇幅將給出一個在Glide中如何生成cache key大致的方案,並且如何讓快取更好的為你工作給一些建議提醒。 Cache Keys: 在Glide中Cache Keys用於DiskCacheS

Android圖片快取分析優化

protected int sizeOf(String key, Drawable value) { if(value!=null) { if (value instanceof BitmapDrawable) { Bitmap bitmap = ((BitmapDraw

[轉載]熱血傳奇資源文件地圖的讀取分析

thead open pda exc height 保留字 img 單位 累加 Mr.Johness阿何的程序人生JMir——Java版熱血傳奇2之資源文件與地圖  我雖然是90後,但是也很喜歡熱血傳奇2(以下簡稱“傳奇”)這款遊戲。  進入程序員行業後自己也對傳奇客戶端實

01 整合IDEA+Maven+SSM框架的高並發的商品秒殺項目業務分析DAO層

初始 lob 可能 很多 ont 配置 支持 個數 base 作者:nnngu 項目源代碼:https://github.com/nnngu/nguSeckill 這是一個整合IDEA+Maven+SSM框架的高並發的商品秒殺項目。我們將分為以下幾篇文章來進行詳細的講解:

BLE4.0教程二 藍牙協議服務特征值分析

cli rac info onf eric ack 而已 訪問 搭建 1.關於服務與特征值的簡述 之前說到藍牙的連接過程,那藍牙連接之後具體是如何傳數據的呢。這裏做一下簡要說明。 藍牙4.0是以參數來進行數據傳輸的,即服務端定好一個參數,客戶端可以對這個參數進行

Android框架VolleyGlide

cat name 圖片緩存 上傳文件 bsp 怎麽 每一個 ons exc PS:在看到這個題目的同時,你們估計會想,Volley與Glide怎麽拿來一塊說呢,他們雖然不是一個框架,但有著相同功能,那就是圖片處理方面。首先我們先來看一下什麽volley,又什麽是glide。

數據結構算法學習筆記如何分析一個排序算法?

編號 height href eight 代碼 [] www. 價值 它的 前言 現在IT這塊找工作,不會幾個算法都不好意思出門,排序算法恰巧是其中最簡單的,我接觸的第一個算法就是它,但是你知道怎麽分析一個排序算法麽?有很多時間復雜度相同的排序算法,在實際編碼中,那又如何

AndroidJSJsBridge使用原始碼分析

在Android開發中,由於Native開發的成本較高,H5頁面的開發更靈活,修改成本更低,因此前端網頁JavaScript(下面簡稱JS)與Java之間的互相呼叫越來越常見。 JsBridge就是一個簡化Android與JS通訊的框架,原始碼:https://github.com/lzyzsd

演算法分析設計多處最優服務次序問題

#include <iostream> #include <algorithm> #include <cstring> #include <cstdio> using namespace std; int main() { int i,n,j,k

(待續)科學計算MATLAB語言資料分析

MATLAB資料分析專題 主要內容: 資料統計分析 多項式計算 資料插值 曲線擬合 第一節 資料統計分析 1)最值 求矩陣的最大和最小元素 max( ): 求向量或矩陣的最大元素 min( ): 求向

演算法分析設計多處最優服務次序問題2

¢ 設有n個顧客同時等待一項服務,顧客i需要的服務時間為ti,1≤i≤n,共有s處可以提供此項服務。應如何安排n個顧客的服務次序才能使平均等待時間達到最小?平均等待時間是n個顧客等待服務時間的總和除以n。 ¢ 給定的n個顧客需要的服務時間和s的值,程式設計計算最優服務次序。 ¢ 輸入 第一行

算法分析設計多處最優服務次序問題2

循環 sin bsp 一行 print include 對比 進行 ios ¢ 設有n個顧客同時等待一項服務,顧客i需要的服務時間為ti,1≤i≤n,共有s處可以提供此項服務。應如何安排n個顧客的服務次序才能使平均等待時間達到最小?平均等待時間是n個顧客等待服務時間的總和

統計分析ROC曲線多指標聯合分析——附SPSS繪製ROC曲線指南

       在進行某診斷方法的評估是,我們常常要用到ROC曲線。這篇博文將簡要介紹ROC曲線以及用SPSS及medcal繪製ROC曲線的方法。 定義        ROC受試者工作特徵曲線 (receive