Glide源碼導讀

来源:http://www.cnblogs.com/angeldevil/archive/2016/09/05/5841979.html
-Advertisement-
Play Games

在這篇文章里,我會介紹下Glide中的一些關鍵概念,並走一遍圖片載入流程,如果你要閱讀Glide源碼的話,應該多少會有點幫助。 ...


最近比較無聊,為了找點事乾,就花了兩天時間把Glide的源碼大概看了一下。剛開始看Glide的源碼頭腦還是比較亂的,因為作者引入了幾個概念,又大量用了泛型,如果不瞭解這些概念讀起代碼來就比較痛苦,我也沒有詳細看各種實現細節的東西,只是瞭解了下這個框架的大概樣子,在這篇文章里,我會介紹下Glide中的一些關鍵概念,並走一遍圖片載入流程,如果你要閱讀Glide源碼的話,應該多少會有點幫助。

基本概念

首先是三個最基本的概念:Model, DataResource

想一下,我們載入圖片需要什麼?一般是一個url,但url並不是所有情況,還有資源ID,文件等等,甚至可以是Feed流中的一條Feed,雖然一般我們會從Feed中取出圖片的url來轉換為從url中載入的情況,Glide把這些抽像為了一個概念,就是Model,所以Model就是數據地址的最初來源。

Model並不能直接解析為圖片,比如一個url,是要轉換為網路流的InputStream才能被解析為圖片的,Model需要進行一次轉換才能做為數據解析的數據源,這些轉換後的東西就叫做Data,Glide並沒有一個Data類,但有很多和它相關的概念,如dataClase,DataFetcher等。

那麼Resource呢,其實它就是一個包裝類,一個wrapper,它wrap一個對象,使這個對象可以通過對象池進行緩存與重用。

這三個基本概念介紹完了,接下來看一下Glide基本框架。

做為一個圖片載入框架,肯定會包含緩存部分。

可以從網上很容易的瞭解到,Glide的磁碟緩存可以緩存原始數據,也可以緩存處理過的數據。什麼意思呢,就是你有一張1000x1000的圖片,但你是在列表中展示的,比如是200x200,那麼緩存時可以直接將整個網路流緩存下來,即1000x1000的圖片,要展示的時候再縮放,但這就降低了展示效率,所以Glide也可以把處理過的200x200的圖片緩存起來,增加了緩存大小,但優化了展示速度。

至於怎麼把數據緩存到磁碟,就引入了一個叫Encoder的概念,Encoder是用來持久化數據的。

但看源碼時你會發現,Glide中有一個類叫Registry,可以註冊多個Encoder,但你會發現它還可以註冊ResourceEncoder。這兩個Encoder很容易引起混淆,而其實ResouseEncoder繼承自EncoderEncoder是用來持久化Data的,ResourceEncoder是用來持久化Resource的。看Glide預設註冊的Encoder就知道了,預設註冊的EncoderByteBufferInputStream,而ResourceEncoderBitmapBitmapDrawableGifDrawable,也就是一個持久化原始數據,一個持久化處理過的數據。我感覺把Encoder做為一個上級的抽象,引入一個和ResourceEncoder同級的DataEncoder就好理解了,正好和前面的基本概念DataResource對應。

Encoder就有Decoder,對應的類叫ResourceDecoder,用來將數據(InputStream等)解析為Resource

圖片載入出來後還可能會應用各種變換,如圓角圖片,圓形圖片,處理這部分工作的叫Transformation

基礎概念介紹的差不多了,載入流程也差不多出來了:

sequence1

但我們發現前面的介紹中少了一環,即:Glide是怎麼把Model轉換為Data的。這就引入另一個概念,ModelLoader,就是把Model轉換成Data的,為了方便說明,直接把這個類的代碼貼上來了,去掉了一些註釋。

/**
* A factory interface for translating an arbitrarily complex data model into a concrete data type
* that can be used by an {@link DataFetcher} to obtain the data for a resource represented by the
* model.
*
* @param <Model> The type of the model.
* @param <Data>  The type of the data that can be used by a
* {@link com.bumptech.glide.load.ResourceDecoder} to decode a resource.
*/
public interface ModelLoader<Model, Data> {

 /**
  * Contains a set of {@link com.bumptech.glide.load.Key Keys} identifying the source of the load,
  * alternate cache keys pointing to equivalent data, and a
  * {@link com.bumptech.glide.load.data.DataFetcher} that can be used to fetch data not found in
  * cache.
  *
  * @param <Data> The type of data that well be loaded.
  */
 class LoadData<Data> {
   public final Key sourceKey;
   public final List<Key> alternateKeys;
   public final DataFetcher<Data> fetcher;

   public LoadData(Key sourceKey, DataFetcher<Data> fetcher) {
     this(sourceKey, Collections.<Key>emptyList(), fetcher);
   }

   public LoadData(Key sourceKey, List<Key> alternateKeys, DataFetcher<Data> fetcher) {
     this.sourceKey = Preconditions.checkNotNull(sourceKey);
     this.alternateKeys = Preconditions.checkNotNull(alternateKeys);
     this.fetcher = Preconditions.checkNotNull(fetcher);
   }
 }

 LoadData<Data> buildLoadData(Model model, int width, int height, Options options);

 boolean handles(Model model);
}

ModelLoader有兩個方法,一個handles表示是否可以處理這個類型的Model,如果可以的話就可以通過buildLoadData生成一個LoadData,而LoadData包含了要用來做緩存的key,及用來獲取數據的DataFetcher

到這裡,整個載入流程就清楚了:

sequence2

基本載入流程

接下來要做的就是根據我們的使用方法走一遍流程,調用如下:

Glide.with(mContext)
    .load(url)
    .apply(RequestOptions.placeholderOf(R.drawable.loading))
    .into(myImageView);

一步步看,先是Glide.with(mContext)

public static RequestManager with(Context context) {
 RequestManagerRetriever retriever = RequestManagerRetriever.get();
 return retriever.get(context);
}

通過RequestManagerRetriever獲取到了一個RequestManager,至於為什麼還需要一個RequestManagerRetriever並有各種重載方法,主要是因為Glide通過SupportRequestManagerFragmentRequestManagerFragment關聯了Activity或Fragment的生命周期,用來做pauseRequests等操作。

然後是load

public RequestBuilder<Drawable> load(@Nullable Object model) {
 return asDrawable().load(model);
}

public RequestBuilder<Drawable> asDrawable() {
 return as(Drawable.class).transition(new DrawableTransitionOptions());
}

public <ResourceType> RequestBuilder<ResourceType> as(Class<ResourceType> resourceClass) {
 return new RequestBuilder<>(glide.getGlideContext(), this, resourceClass);
}

asDrawable.load(model)的縮寫,就是說這個Model我是要載入為Drawable的,最終返回一個RequestBuilder,看名字就知道是做什麼了,不過這個類主要是設置Thumbnail Request,Transition等個別設置(舊版本中placeHolder等也是在這裡設置的),大部分設置在RequestOptions里,這就是下麵這一句:

apply(RequestOptions.placeholderOf(R.drawable.loading))

應用一個RequestOptionsRequestOptions可以設置各種請求相關的選項,如占點陣圖片,載入失敗的圖片,緩存策略等。RequestOptions繼承自BaseRequestOptions,但全是工廠方法生成各種RequestOptions。

最後就是into了,把圖片載入到一個Target中。

public Target<TranscodeType> into(ImageView view) {
  ...
  return into(context.buildImageViewTarget(view, transcodeClass));
}

public <Y extends Target<TranscodeType>> Y into(@NonNull Y target) {
 Util.assertMainThread();
 Preconditions.checkNotNull(target);
 if (!isModelSet) {
   throw new IllegalArgumentException("You must call #load() before calling #into()");
 }

 Request previous = target.getRequest();

 if (previous != null) {
   requestManager.clear(target);
 }

 requestOptions.lock();
 Request request = buildRequest(target);
 target.setRequest(request);
 requestManager.track(target, request);

 return target;
}

Target是要載入到的目標,比如ImageViewTargetAppWidgetTarget,在這裡我們傳進來了一個ImageView,內部生成了一個DrawableImageViewTarget。這裡最主要的操作是buildRequest然後交給RequestManagertrack

void track(Target<?> target, Request request) {
 targetTracker.track(target);
 requestTracker.runRequest(request);
}

// RequestTracker
public void runRequest(Request request) {
 requests.add(request);
 if (!isPaused) {
   request.begin();
 } else {
   pendingRequests.add(request);
 }
}

TargetTracker主要就是記錄一下所有正在載入的圖片的Target,所以載入行為是在RequestTracker.runRequest中的,runRequest先判斷是否是pause狀態(RequestManager設置),如果不是就直接調用Request.begin觸發載入,否則就回到pending隊列里等待resume。

除了設置縮略圖的情景,使用的Request都是SingleRequest,看一下它的begin方法:

public void begin() {
 stateVerifier.throwIfRecycled();
 startTime = LogTime.getLogTime();
 if (model == null) {
   if (Util.isValidDimensions(overrideWidth, overrideHeight)) {
     width = overrideWidth;
     height = overrideHeight;
   }
   // Only log at more verbose log levels if the user has set a fallback drawable, because
   // fallback Drawables indicate the user expects null models occasionally.
   int logLevel = getFallbackDrawable() == null ? Log.WARN : Log.DEBUG;
   onLoadFailed(new GlideException("Received null model"), logLevel);
   return;
 }

 status = Status.WAITING_FOR_SIZE;
 if (Util.isValidDimensions(overrideWidth, overrideHeight)) {
   onSizeReady(overrideWidth, overrideHeight);
 } else {
   target.getSize(this);
 }

 if ((status == Status.RUNNING || status == Status.WAITING_FOR_SIZE)
     && canNotifyStatusChanged()) {
   target.onLoadStarted(getPlaceholderDrawable());
 }
 if (Log.isLoggable(TAG, Log.VERBOSE)) {
   logV("finished run method in " + LogTime.getElapsedMillis(startTime));
 }
}

載入邏輯是這幾行:

 if (Util.isValidDimensions(overrideWidth, overrideHeight)) {
   onSizeReady(overrideWidth, overrideHeight);
 } else {
   target.getSize(this);
 }

判斷下是否知道Target的大小,如果大小已知就調用onSizeReady,否則就調用target.getSize獲取它的大小,當成功獲取到大小後,會通過回調繼續調用onSizeReady,所以整個載入方法都是在onSizeReady里的。至於Target怎麼獲取它的大小,那要看它的實現了,對於ImageViewTarget,是通過ViewTreeObserver.OnPreDrawListener等到View要測繪的時候就知道它的大小了。

onSizeReady就是把操作轉移到了Engine.load

public <R> LoadStatus load(
   GlideContext glideContext,
   Object model,
   Key signature,
   int width,
   int height,
   Class<?> resourceClass,
   Class<R> transcodeClass,
   Priority priority,
   DiskCacheStrategy diskCacheStrategy,
   Map<Class<?>, Transformation<?>> transformations,
   boolean isTransformationRequired,
   Options options,
   boolean isMemoryCacheable,
   boolean useUnlimitedSourceExecutorPool,
   ResourceCallback cb) {
 Util.assertMainThread();
 long startTime = LogTime.getLogTime();

 EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations,
     resourceClass, transcodeClass, options);

 EngineResource<?> cached = loadFromCache(key, isMemoryCacheable);
 if (cached != null) {
   cb.onResourceReady(cached, DataSource.MEMORY_CACHE);
   if (Log.isLoggable(TAG, Log.VERBOSE)) {
     logWithTimeAndKey("Loaded resource from cache", startTime, key);
   }
   return null;
 }

 EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable);
 if (active != null) {
   cb.onResourceReady(active, DataSource.MEMORY_CACHE);
   if (Log.isLoggable(TAG, Log.VERBOSE)) {
     logWithTimeAndKey("Loaded resource from active resources", startTime, key);
   }
   return null;
 }

 EngineJob<?> current = jobs.get(key);
 if (current != null) {
   current.addCallback(cb);
   if (Log.isLoggable(TAG, Log.VERBOSE)) {
     logWithTimeAndKey("Added to existing load", startTime, key);
   }
   return new LoadStatus(cb, current);
 }

 EngineJob<R> engineJob = engineJobFactory.build(key, isMemoryCacheable,
     useUnlimitedSourceExecutorPool);
 DecodeJob<R> decodeJob = decodeJobFactory.build(
     glideContext,
     model,
     key,
     signature,
     width,
     height,
     resourceClass,
     transcodeClass,
     priority,
     diskCacheStrategy,
     transformations,
     isTransformationRequired,
     options,
     engineJob);
 jobs.put(key, engineJob);
 engineJob.addCallback(cb);
 engineJob.start(decodeJob);

 if (Log.isLoggable(TAG, Log.VERBOSE)) {
   logWithTimeAndKey("Started new load", startTime, key);
 }
 return new LoadStatus(cb, engineJob);
}

Engine.load中,先loadFromCache,如果緩存沒有命中就再loadFromActiveResources,這是兩級記憶體緩存,第一級是LruCache,第二級是ActiveCache,主要作用是,有可能一個圖片很早就被載入了,可能已經從LruCache被移除掉了,但這個圖片可能還在被某一個地方引用著,也就是還是Active的,那它就可能在將來仍被引用到,所以就把它保留在二級的ActiveCache中,ActiveCache中是以弱引用引用圖片的,並通過ReferenceQueue監測弱引用的回收,然後用Handler.IdleHandler在CPU空閑時被被回收的引用項從ActiveCache中移除。

接下來看對應的Key是否已經正在載入,如果是的話,就addCallback,這樣如果有多個地方同時請求同一張圖片的話,只會生成一個載入任務,並都能收到回調,這點是比Universal-Image-Loader好的地方。

正常的載入流程是生成一個EngineJob和一個DecodeJob,通過engineJob.start(decodeJob)來進行實際的載入。

public void start(DecodeJob<R> decodeJob) {
 this.decodeJob = decodeJob;
 GlideExecutor executor = decodeJob.willDecodeFromCache()
     ? diskCacheExecutor
     : getActiveSourceExecutor();
 executor.execute(decodeJob);
}

EngineJob.start直接將DecodeJob交給Executor去執行了(DecodeJob實現了Runnable介面)。DecodeJob的載入操作放到了runWrapped

private void runWrapped() {
  switch (runReason) {
   case INITIALIZE:
     stage = getNextStage(Stage.INITIALIZE);
     currentGenerator = getNextGenerator();
     runGenerators();
     break;
   case SWITCH_TO_SOURCE_SERVICE:
     runGenerators();
     break;
   case DECODE_DATA:
     decodeFromRetrievedData();
     break;
   default:
     throw new IllegalStateException("Unrecognized run reason: " + runReason);
 }
}

private DataFetcherGenerator getNextGenerator() {
 switch (stage) {
   case RESOURCE_CACHE:
     return new ResourceCacheGenerator(decodeHelper, this);
   case DATA_CACHE:
     return new DataCacheGenerator(decodeHelper, this);
   case SOURCE:
     return new SourceGenerator(decodeHelper, this);
   case FINISHED:
     return null;
   default:
     throw new IllegalStateException("Unrecognized stage: " + stage);
 }
}

private Stage getNextStage(Stage current) {
 switch (current) {
   case INITIALIZE:
     return diskCacheStrategy.decodeCachedResource()
         ? Stage.RESOURCE_CACHE : getNextStage(Stage.RESOURCE_CACHE);
   case RESOURCE_CACHE:
     return diskCacheStrategy.decodeCachedData()
         ? Stage.DATA_CACHE : getNextStage(Stage.DATA_CACHE);
   case DATA_CACHE:
     return Stage.SOURCE;
   case SOURCE:
   case FINISHED:
     return Stage.FINISHED;
   default:
     throw new IllegalArgumentException("Unrecognized stage: " + current);
 }
}

主要載入邏輯就在這三個函數中了:

  1. 先獲取當前的Stage
  2. 根據當前的Stage獲取相應的Generator,
  3. 執行Generator

一共有三種Generator:

  • ResourceCacheGenerator:從處理過的緩存載入數據
  • DataCacheGenerator:從原始緩存載入數據
  • SourceGenerator:從數據源請求數據,如網路請求

前面說過,Glide的磁碟緩存可以選擇緩存原始圖片,緩存處理過的圖片(如列表中顯示縮略圖時縮放後的圖片),這三個Generator就分別對應處理過的圖片緩存,原始圖片緩存,和數據源載入。

在上面的第三步執行Generator時主要就是調用了Generator,其實就是執行Generator的startNext方法,這裡以SourceGenerator為例。

public boolean startNext() {
 if (dataToCache != null) {
   Object data = dataToCache;
   dataToCache = null;
   cacheData(data);
 }

 if (sourceCacheGenerator != null && sourceCacheGenerator.startNext()) {
   return true;
 }
 sourceCacheGenerator = null;

 loadData = null;
 boolean started = false;
 while (!started && hasNextModelLoader()) {
   loadData = helper.getLoadData().get(loadDataListIndex++);
   if (loadData != null
       && (helper.getDiskCacheStrategy().isDataCacheable(loadData.fetcher.getDataSource())
       || helper.hasLoadPath(loadData.fetcher.getDataClass()))) {
     started = true;
     loadData.fetcher.loadData(helper.getPriority(), this);
   }
 }
 return started;
}

先忽略函數開始時dataToCachesourceCacheGenerator相關的代碼,第一次載入時這兩個一定是null的。剩下的流程就是獲取一個LoadData,調用LoadData.fetcher.loadData載入數據。看一下LoadData

List<LoadData<?>> getLoadData() {
 if (!isLoadDataSet) {
   isLoadDataSet = true;
   loadData.clear();
   List<ModelLoader<Object, ?>> modelLoaders = glideContext.getRegistry().getModelLoaders(model);
   int size = modelLoaders.size();
   for (int i = 0; i < size; i++) {
     ModelLoader<Object, ?> modelLoader = modelLoaders.get(i);
     LoadData<?> current =
         modelLoader.buildLoadData(model, width, height, options);
     if (current != null) {
       loadData.add(current);
     }
   }
 }
 return loadData;
}

getLoadData中通過獲取所有提前註冊過的能處理Model類型的ModelLoader,調用它的buildLoadData生成LoadData,最終返回一個LoadData列表。

前面說過LoadData包含了用來獲取數據的DataFetcherSourceGenerator.startNext就調用了loadData.fetcher.loadData來進行載入數據,並傳進去一個Callback,就是當前的SourceGenerator,如果載入成功,會調用onDataReady

public void onDataReady(Object data) {
 DiskCacheStrategy diskCacheStrategy = helper.getDiskCacheStrategy();
 if (data != null && diskCacheStrategy.isDataCacheable(loadData.fetcher.getDataSource())) {
   dataToCache = data;
   // We might be being called back on someone else's thread. Before doing anything, we should
   // reschedule to get back onto Glide's thread.
   cb.reschedule();
 } else {
   cb.onDataFetcherReady(loadData.sourceKey, data, loadData.fetcher,
       loadData.fetcher.getDataSource(), originalKey);
 }
}

數據載入成功後,如果設置了要進行磁碟緩存,會設置成員變數dataToCache,並調用Callback的reschedule,結果就是會再次調用當前Generator的startNextstartNext的前半部分實現就起作用了,會進行寫緩存的操作。

rescheudle後寫了緩存後,或不緩存的情況下,會調用onDataFetcherReady,這個Callback就是前面的DecodeJob,在onDataFetcherReady中會調用decodeFromRetrievedData decode數據,最終調用到decodeFromFetcher

private <Data> Resource<R> decodeFromFetcher(Data data, DataSource dataSource)
   throws GlideException {
 LoadPath<Data, ?, R> path = decodeHelper.getLoadPath((Class<Data>) data.getClass());
 return runLoadPath(data, dataSource, path);
}

獲取LoadPath,並調用它的load方法。LoadPath就是封裝了多個DecodePathDecodePath用於decode and Transform數據,如InputStream->Bitmap->BitmapDrawable,DecodePath中會獲取預先註冊的Decoder來decode獲取到的數據,decode成功後通過回調調用DecodeJobonResourceDecoded方法。

public Resource<Z> onResourceDecoded(Resource<Z> decoded) {
 Class<Z> resourceSubClass = getResourceClass(decoded);
 Transformation<Z> appliedTransformation = null;
 Resource<Z> transformed = decoded;
 if (dataSource != DataSource.RESOURCE_DISK_CACHE) {
   appliedTransformation = decodeHelper.getTransformation(resourceSubClass);
   transformed = appliedTransformation.transform(decoded, width, height);        //////////////////////////    1
 }
 // TODO: Make this the responsibility of the Transformation.
 if (!decoded.equals(transformed)) {
   decoded.recycle();
 }

 final EncodeStrategy encodeStrategy;
 final ResourceEncoder<Z> encoder;
 if (decodeHelper.isResourceEncoderAvailable(transformed)) {
   encoder = decodeHelper.getResultEncoder(transformed);
   encodeStrategy = encoder.getEncodeStrategy(options);
 } else {
   encoder = null;
   encodeStrategy = EncodeStrategy.NONE;
 }

 Resource<Z> result = transformed;
 boolean isFromAlternateCacheKey = !decodeHelper.isSourceKey(currentSourceKey);
 if (diskCacheStrategy.isResourceCacheable(isFromAlternateCacheKey, dataSource,
     encodeStrategy)) {
   if (encoder == null) {
     throw new Registry.NoResultEncoderAvailableException(transformed.get().getClass());
   }
   final Key key;
   if (encodeStrategy == EncodeStrategy.SOURCE) {
     key = new DataCacheKey(currentSourceKey, signature);
   } else if (encodeStrategy == EncodeStrategy.TRANSFORMED) {
     key = new ResourceCacheKey(currentSourceKey, signature, width, height,
         appliedTransformation, resourceSubClass, options);
   } else {
     throw new IllegalArgumentException("Unknown strategy: " + encodeStrategy);
   }

   LockedResource<Z> lockedResult = LockedResource.obtain(transformed);
   deferredEncodeManager.init(key, encoder, lockedResult);           //////////////////////////    2
   result = lockedResult;
 }
 return result;
}

在上述代碼的註釋1處對載入成功的資源應用Transformation,然後在註釋2處根據緩存策略初始化DeferredEncodeManager,在前面的decodeFromRetrievedData中,如果有必要會把transform過的資源寫緩存。

private void decodeFromRetrievedData() {
  ...

 if (resource != null) {
   notifyEncodeAndRelease(resource, currentDataSource);
 } else {
   runGenerators();
 }
}

notifyEncodeAndRelease中處理了對處理過的圖片的緩存操作。當緩存完成後(如果有需要的話)就通過回調告訴外面載入完成了。至此,整個載入過程完成。

Glide配置

Glide允許我們進行一定程度的自定義,比如設置自定義的Executor,設置緩存池,設置Log等級等,完成這個任務的類叫GlideBuilder,Glide類在工程中是作為單例使用的,看一下代碼:

public static Glide get(Context context) {
 if (glide == null) {
   synchronized (Glide.class) {
     if (glide == null) {
       Context applicationContext = context.getApplicationContext();
       List<GlideModule> modules = new ManifestParser(applicationContext).parse();

       GlideBuilder builder = new GlideBuilder(applicationContext);
       for (GlideModule module : modules) {
         module.applyOptions(applicationContext, builder);
       }
       glide = builder.createGlide();
       for (GlideModule module : modules) {
         module.registerComponents(applicationContext, glide.registry);
       }
     }
   }
 }

 return glide;
}

通過GlideBuilder生成了一個Glide實例,我們是沒有辦法直接配置GlideBuilder的,但我們發現Glide.get解析了Manifest,獲取了一個GlideModule的列表,並調用了它的applyOptionsregisterComponents方法。以項目中OkHttp的配置為例

public class OkHttpGlideModule implements GlideModule {
 @Override
 public void applyOptions(Context context, GlideBuilder builder) {
   // Do nothing.
 }

 @Override
 public void registerComponents(Context context, Registry registry) {
   registry.replace(GlideUrl.class, InputStream.class, new OkHttpUrlLoader.Factory());
 }
}

GlideModule有兩個方法,applyOptions,有一個GlideBuilder參數,在這裡我們就可以配置Glide了。還有一個registerComponents方法,並有一個Registry參數,通過這個類的實例我們就可以註冊我們自定義的ModelLoaderEncoder等基礎組件了。

自定義GlideModule是通過Manifest的meta-data標簽配置的

<meta-data
   android:name="com.bumptech.glide.integration.okhttp3.OkHttpGlideModule"
   android:value="GlideModule"/>

參考資料

http://www.lightskystreet.com/2015/10/12/glide_source_analysis/


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 在Android Studio中運行APP時出現了以下錯誤: 解決的辦法是點擊:tools ->Android->sync project with gradles files! ...
  • 一、引言 如今,Android+html5開發已經成為最流行的開發模式。 Android 中可以通過webview來實現和js的交互,在程式中調用js代碼,只需要將webview控制項的支持js的屬性設置為true Android(Java)與JavaScript(HTML)交互有四種情況: 1) A ...
  • 新版中的TimePicker DatePicker是不支持使用遙控器的, 恢覆成低版本滾動模式只需要是xml文件加上一句即可: 本文為博主原創文章,轉載請註明出處 http://www.cnblogs.com/rencm/p/5842798.html ...
  • 1、Android 中的Json解析工具fastjson 、序列化、反序列化 2、Android Gson的使用總結 3、Android-JSONTool 一個簡易的Json框架類,小到只有一個類 有時為了簡化代碼的大小,儘可能的壓縮apk的大小。就不能再使用大而全的框架了。 ...
  • 一:理論知識點 1:什麼是FlexBox佈局? 彈性盒模型(The Flexible Box Module),又叫Flexbox,意為“彈性佈局”,旨在通過彈性的方式來對齊和分佈容器中內容的空間,使其能適應不同屏幕,為盒裝模型提供最大的靈活性。 Flex佈局主要思想是:讓容器有能力讓其子項目能夠改變 ...
  • 1、概念 Gson是谷歌發佈的一個json解析框架 2、如何獲取 github:https://github.com/google/gson android studio使用 查看 最新版本號 下載最新的jar包 ,http://search.maven.org/#search%7Cga%7C1%7 ...
  • 使用系統自帶生成/掃描二維碼iOS7開始蘋果集成了二維碼的生成的掃描### 生成二維碼的步驟導入CoreImage框架 #import 通過濾鏡CIFilte生成二維碼### 二維碼的內容(傳統的條形碼只能放數字)純文本名片URL生成二維碼 // 1.創建過濾器 CIFilter *filter =... ...
  • 1、Android一整套圖片解決方案 http://mp.weixin.qq.com/s?__biz=MzAxMTI4MTkwNQ==&mid=2650820998&idx=1&sn=c9670674dcfb71a24521e898776f234e&scene=1&srcid=0905yknSzNO ...
一周排行
    -Advertisement-
    Play Games
  • 前言 推薦一款基於.NET 8、WPF、Prism.DryIoc、MVVM設計模式、Blazor以及MySQL資料庫構建的企業級工作流系統的WPF客戶端框架-AIStudio.Wpf.AClient 6.0。 項目介紹 框架採用了 Prism 框架來實現 MVVM 模式,不僅簡化了 MVVM 的典型 ...
  • 先看一下效果吧: 我們直接通過改造一下原版的TreeView來實現上面這個效果 我們先創建一個普通的TreeView 代碼很簡單: <TreeView> <TreeViewItem Header="人事部"/> <TreeViewItem Header="技術部"> <TreeViewItem He ...
  • 1. 生成式 AI 簡介 https://imp.i384100.net/LXYmq3 2. Python 語言 https://imp.i384100.net/5gmXXo 3. 統計和 R https://youtu.be/ANMuuq502rE?si=hw9GT6JVzMhRvBbF 4. 數 ...
  • 本文為大家介紹下.NET解壓/壓縮zip文件。雖然解壓縮不是啥核心技術,但壓縮性能以及進度處理還是需要關註下,針對使用較多的zip開源組件驗證,給大家提供個技術選型參考 之前在《.NET WebSocket高併發通信阻塞問題 - 唐宋元明清2188 - 博客園 (cnblogs.com)》講過,團隊 ...
  • 之前寫過兩篇關於Roslyn源生成器生成源代碼的用例,今天使用Roslyn的代碼修複器CodeFixProvider實現一個cs文件頭部註釋的功能, 代碼修複器會同時涉及到CodeFixProvider和DiagnosticAnalyzer, 實現FileHeaderAnalyzer 首先我們知道修 ...
  • 在軟體行業,經常會聽到一句話“文不如表,表不如圖”說明瞭圖形在軟體應用中的重要性。同樣在WPF開發中,為了程式美觀或者業務需要,經常會用到各種個樣的圖形。今天以一些簡單的小例子,簡述WPF開發中幾何圖形(Geometry)相關內容,僅供學習分享使用,如有不足之處,還請指正。 ...
  • 在 C# 中使用 RabbitMQ 通過簡訊發送重置後的密碼到用戶的手機號上,你可以按照以下步驟進行 1.安裝 RabbitMQ 客戶端庫 首先,確保你已經安裝了 RabbitMQ 客戶端庫。你可以通過 NuGet 包管理器來安裝: dotnet add package RabbitMQ.Clien ...
  • 1.下載 Protocol Buffers 編譯器(protoc) 前往 Protocol Buffers GitHub Releases 頁面。在 "Assets" 下找到適合您系統的壓縮文件,通常為 protoc-{version}-win32.zip 或 protoc-{version}-wi ...
  • 簡介 在現代微服務架構中,服務發現(Service Discovery)是一項關鍵功能。它允許微服務動態地找到彼此,而無需依賴硬編碼的地址。以前如果你搜 .NET Service Discovery,大概率會搜到一大堆 Eureka,Consul 等的文章。現在微軟為我們帶來了一個官方的包:Micr ...
  • ZY樹洞 前言 ZY樹洞是一個基於.NET Core開發的簡單的評論系統,主要用於大家分享自己心中的感悟、經驗、心得、想法等。 好了,不賣關子了,這個項目其實是上班無聊的時候寫的,為什麼要寫這個項目呢?因為我單純的想吐槽一下工作中的不滿而已。 項目介紹 項目很簡單,主要功能就是提供一個簡單的評論系統 ...