Android圖片加載框架Gilde源碼層深入分析
1.使用Gilde顯示一張圖片
Glide.with(this).
load("https://cn.bing.com/sa/simg/hpb/xxx.jpg")
.into(imageView);
上邊是Glide最簡單的來顯示一張圖片,雖然只有三步操作:with、load、into,但是gilde卻通過大量的代碼在維護著。
with:返回一個RequestManager
load:返回一個RequestBuilder
下面通過源碼分析這三塊,重點是Glide生明周期的管理和緩存的使用,以及整個加載的流程的梳理。
2.Glide with操作源碼解析
Glide的with函數(shù)由于參數(shù)類型原因有多個重載,下面選擇傳入Activity當(dāng)做參數(shù)的
public static RequestManager with(@NonNull Activity activity) {
return getRetriever(activity).get(activity);
}
這個方法就是得到一個RequestManager
分析getRetriever
private static RequestManagerRetriever getRetriever(@Nullable Context context) {
Preconditions.checkNotNull(
context,
"You cannot start a load on a not yet attached View or a Fragment where getActivity() "
+ "returns null (which usually occurs when getActivity() is called before the Fragment "
+ "is attached or after the Fragment is destroyed).");
return Glide.get(context).getRequestManagerRetriever();
}
得到一個RequestManagerRetriever,這個管理RequestManager
RequestManagerRetriever的get方法
public RequestManager get(@NonNull Context context) {
if (context == null) {
throw new IllegalArgumentException("You cannot start a load on a null Context");
} else if (Util.isOnMainThread() && !(context instanceof Application)) {
if (context instanceof FragmentActivity) {
return get((FragmentActivity) context);
} else if (context instanceof Activity) {
return get((Activity) context);
} else if (context instanceof ContextWrapper
// Only unwrap a ContextWrapper if the baseContext has a non-null application context.
// Context#createPackageContext may return a Context without an Application instance,
// in which case a ContextWrapper may be used to attach one.
&& ((ContextWrapper) context).getBaseContext().getApplicationContext() != null) {
return get(((ContextWrapper) context).getBaseContext());
}
}
return getApplicationManager(context);
}
根據(jù)不同的參數(shù)類型,執(zhí)行不同的代碼邏輯。
參數(shù)是:FragmentActivity的源碼
public RequestManager get(@NonNull FragmentActivity activity) {
if (Util.isOnBackgroundThread()) {
return get(activity.getApplicationContext());
} else {
assertNotDestroyed(activity);
frameWaiter.registerSelf(activity);
FragmentManager fm = activity.getSupportFragmentManager();
return supportFragmentGet(activity, fm, /*parentHint=*/ null, isActivityVisible(activity));
}
}
注意這里判斷了線程,如果是子線程會直接走ApplicationContext類型的函數(shù)重載。
supportFragmentGet方法:
private RequestManager supportFragmentGet(
@NonNull Context context,
@NonNull FragmentManager fm,
@Nullable Fragment parentHint,
boolean isParentVisible) {
SupportRequestManagerFragment current = getSupportRequestManagerFragment(fm, parentHint);
RequestManager requestManager = current.getRequestManager();
if (requestManager == null) {
// TODO(b/27524013): Factor out this Glide.get() call.
Glide glide = Glide.get(context);
requestManager =
factory.build(
glide, current.getGlideLifecycle(), current.getRequestManagerTreeNode(), context);
// This is a bit of hack, we're going to start the RequestManager, but not the
// corresponding Lifecycle. It's safe to start the RequestManager, but starting the
// Lifecycle might trigger memory leaks. See b/154405040
if (isParentVisible) {
requestManager.onStart();
}
current.setRequestManager(requestManager);
}
return requestManager;
}
這個方法會創(chuàng)建一個空白的SupportRequestManagerFragment,這個Fragment的作用很重要,也是Glide的核心,聲明周期的管理就是靠這個來監(jiān)控的。
監(jiān)控了:onStart、onStop、onDestroy,所以Glide在使用的時候,我們不用關(guān)心資源的釋放,因為Glide會自動釋放資源,釋放資源的依據(jù)就是這個聲明周期的管理。
get函數(shù)參數(shù)是:ApplicationContext 的源碼
private RequestManager getApplicationManager(@NonNull Context context) {
// Either an application context or we're on a background thread.
if (applicationManager == null) {
synchronized (this) {
if (applicationManager == null) {
Glide glide = Glide.get(context.getApplicationContext());
applicationManager =
factory.build(
glide,
new ApplicationLifecycle(),
new EmptyRequestManagerTreeNode(),
context.getApplicationContext());
}
}
}
return applicationManager;
}
可見當(dāng)參數(shù)類型是:ApplicationContext或者子線程調(diào)用with方法不會創(chuàng)建空白的Fragmnet來進行生命周期的監(jiān)控,也不會進行資源釋放,所以在調(diào)用with方法的時候不能傳ApplicationContext。
下面分析Glide分別在onStart、onStop、onDestroy執(zhí)行了那些邏輯
RequestManager的onStart方法
public synchronized void onStart() {
resumeRequests();
targetTracker.onStart();
}
執(zhí)行了刷新請求和Tracker的onStart,resumeRequests這個跟請求的緩存的使用
RequestTracker 的resumeRequests方法
public void resumeRequests() {
isPaused = false;
for (Request request : Util.getSnapshot(requests)) {
if (!request.isComplete() && !request.isRunning()) {
request.begin();
}
}
pendingRequests.clear();
}
將運行隊列requests所有沒有完成又處于停止狀態(tài)的任務(wù)全部開始執(zhí)行,requests是一個正在運行的請求緩存集合,這是一個set集合,處于等待狀態(tài)的請求隊列pendingRequests進行清理操作。這個就是Glide感應(yīng)到聲明周期onStart進行的操作。
RequestManager的onStop方法
public synchronized void onStop() {
pauseRequests();
targetTracker.onStop();
}
RequestTracker的pauseRequests方法
public void pauseRequests() {
isPaused = true;
for (Request request : Util.getSnapshot(requests)) {
if (request.isRunning()) {
request.pause();
pendingRequests.add(request);
}
}
}
request.pause():將所有的運行緩存集合中的請求進行暫停操作,并將這些請求加入到等待緩存請求集合pendingRequests
RequestManager的onDestroy
public synchronized void onDestroy() {
targetTracker.onDestroy();
for (Target<?> target : targetTracker.getAll()) {
clear(target);
}
targetTracker.clear();
requestTracker.clearRequests();
lifecycle.removeListener(this);
lifecycle.removeListener(connectivityMonitor);
Util.removeCallbacksOnUiThread(addSelfToLifecycle);
glide.unregisterRequestManager(this);
}
可以看到Glide在感應(yīng)到onDestroy會進行各種資源釋放,監(jiān)聽的移除操作。
3.Glide 的load操作
RequestManager的load方法
public RequestBuilder<Drawable> load(@Nullable Bitmap bitmap) {
return asDrawable().load(bitmap);
}
public RequestBuilder<Drawable> asDrawable() {
return as(Drawable.class);
}
public <ResourceType> RequestBuilder<ResourceType> as(
@NonNull Class<ResourceType> resourceClass) {
return new RequestBuilder<>(glide, this, resourceClass, context);
}
load方法返回值是RequestBuilder,通過new的方式進行創(chuàng)建
RequestBuilder的構(gòu)造方法
protected RequestBuilder(
@NonNull Glide glide,
RequestManager requestManager,
Class<TranscodeType> transcodeClass,
Context context) {
this.glide = glide;
this.requestManager = requestManager;
this.transcodeClass = transcodeClass;
this.context = context;
this.transitionOptions = requestManager.getDefaultTransitionOptions(transcodeClass);
this.glideContext = glide.getGlideContext();
initRequestListeners(requestManager.getDefaultRequestListeners());
apply(requestManager.getDefaultRequestOptions());
}
initRequestListeners:初始化監(jiān)聽
apply:校驗參數(shù)
load的過程比較簡單,重點的into將會非常復(fù)雜
4.Glide的into流程解析
public <Y extends Target<TranscodeType>> Y into(@NonNull Y target) {
return into(target, /*targetListener=*/ null, Executors.mainThreadExecutor());
}
<Y extends Target<TranscodeType>> Y into(
@NonNull Y target,
@Nullable RequestListener<TranscodeType> targetListener,
Executor callbackExecutor) {
return into(target, targetListener, /*options=*/ this, callbackExecutor);
}
into另外一個重載方法:
private <Y extends Target<TranscodeType>> Y into(
@NonNull Y target,
@Nullable RequestListener<TranscodeType> targetListener,
BaseRequestOptions<?> options,
Executor callbackExecutor) {
Preconditions.checkNotNull(target);
if (!isModelSet) {
throw new IllegalArgumentException("You must call #load() before calling #into()");
}
Request request = buildRequest(target, targetListener, options, callbackExecutor);
Request previous = target.getRequest();
if (request.isEquivalentTo(previous)
&& !isSkipMemoryCacheWithCompletePreviousRequest(options, previous)) {
if (!Preconditions.checkNotNull(previous).isRunning()) {
previous.begin();
}
return target;
}
requestManager.clear(target);
target.setRequest(request);
requestManager.track(target, request);
return target;
}buildRequest 會通過SingleRequest.obtain,創(chuàng)建一個SingleRequest,這個必須要前置了解,后邊的流程需要用到。
requestManager.track(target, request):這個是主流程,下面來看RequestManager的track方法
RequestManager的track方法
synchronized void track(@NonNull Target<?> target, @NonNull Request request) {
targetTracker.track(target);
requestTracker.runRequest(request);
}
RequestTracker的runRequest方法
public void runRequest(@NonNull Request request) {
requests.add(request);
if (!isPaused) {
request.begin();
} else {
request.clear();
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Paused, delaying request");
}
pendingRequests.add(request);
}
}
這里的Request是一個接口,調(diào)用了begin方法,這個Request就是前面創(chuàng)建的SingleRequest
SingleRequest的begin
public void begin() {
synchronized (requestLock) {
...
if (status == Status.COMPLETE) {
onResourceReady(
resource, DataSource.MEMORY_CACHE, /* isLoadedFromAlternateCacheKey= */ false);
return;
}
cookie = GlideTrace.beginSectionAsync(TAG);
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 (IS_VERBOSE_LOGGABLE) {
logV("finished run method in " + LogTime.getElapsedMillis(startTime));
}
}
}
如果沒有設(shè)置寬高會調(diào)用 target.getSize(this)來獲取大小,如果設(shè)置了會執(zhí)行onSizeReady方法
SingleRequest的onSizeReady方法
public void onSizeReady(int width, int height) {
stateVerifier.throwIfRecycled();
synchronized (requestLock) {
if (IS_VERBOSE_LOGGABLE) {
logV("Got onSizeReady in " + LogTime.getElapsedMillis(startTime));
}
if (status != Status.WAITING_FOR_SIZE) {
return;
}
status = Status.RUNNING;
float sizeMultiplier = requestOptions.getSizeMultiplier();
this.width = maybeApplySizeMultiplier(width, sizeMultiplier);
this.height = maybeApplySizeMultiplier(height, sizeMultiplier);
if (IS_VERBOSE_LOGGABLE) {
logV("finished setup for calling load in " + LogTime.getElapsedMillis(startTime));
}
loadStatus =
engine.load(
glideContext,
model,
requestOptions.getSignature(),
this.width,
this.height,
requestOptions.getResourceClass(),
transcodeClass,
priority,
requestOptions.getDiskCacheStrategy(),
requestOptions.getTransformations(),
requestOptions.isTransformationRequired(),
requestOptions.isScaleOnlyOrNoTransform(),
requestOptions.getOptions(),
requestOptions.isMemoryCacheable(),
requestOptions.getUseUnlimitedSourceGeneratorsPool(),
requestOptions.getUseAnimationPool(),
requestOptions.getOnlyRetrieveFromCache(),
this,
callbackExecutor);
if (status != Status.RUNNING) {
loadStatus = null;
}
if (IS_VERBOSE_LOGGABLE) {
logV("finished onSizeReady in " + LogTime.getElapsedMillis(startTime));
}
}
}接下來調(diào)用Engin的load方法
Engin的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,
boolean isScaleOnlyOrNoTransform,
Options options,
boolean isMemoryCacheable,
boolean useUnlimitedSourceExecutorPool,
boolean useAnimationPool,
boolean onlyRetrieveFromCache,
ResourceCallback cb,
Executor callbackExecutor) {
long startTime = VERBOSE_IS_LOGGABLE ? LogTime.getLogTime() : 0;
EngineKey key =
keyFactory.buildKey(
model,
signature,
width,
height,
transformations,
resourceClass,
transcodeClass,
options);
EngineResource<?> memoryResource;
synchronized (this) {
memoryResource = loadFromMemory(key, isMemoryCacheable, startTime);
if (memoryResource == null) {
return waitForExistingOrStartNewJob(
glideContext,
model,
signature,
width,
height,
resourceClass,
transcodeClass,
priority,
diskCacheStrategy,
transformations,
isTransformationRequired,
isScaleOnlyOrNoTransform,
options,
isMemoryCacheable,
useUnlimitedSourceExecutorPool,
useAnimationPool,
onlyRetrieveFromCache,
cb,
callbackExecutor,
key,
startTime);
}
}
// Avoid calling back while holding the engine lock, doing so makes it easier for callers to
// deadlock.
cb.onResourceReady(
memoryResource, DataSource.MEMORY_CACHE, /* isLoadedFromAlternateCacheKey= */ false);
return null;
}loadFromMemory這個方法是先從緩存中去查找,如果有直接就可以直接使用了
loadFromMemory方法
@Nullable
private EngineResource<?> loadFromMemory(
EngineKey key, boolean isMemoryCacheable, long startTime) {
if (!isMemoryCacheable) {
return null;
}
EngineResource<?> active = loadFromActiveResources(key);
if (active != null) {
if (VERBOSE_IS_LOGGABLE) {
logWithTimeAndKey("Loaded resource from active resources", startTime, key);
}
return active;
}
EngineResource<?> cached = loadFromCache(key);
if (cached != null) {
if (VERBOSE_IS_LOGGABLE) {
logWithTimeAndKey("Loaded resource from cache", startTime, key);
}
return cached;
}
return null;
}
loadFromActiveResources:從活動緩存查找,如果有直接返回使用,這個緩存是一個Map集合
loadFromCache:從內(nèi)存緩存查找,如果有直接使用,這個是一個LRU集合
上邊兩個是都是內(nèi)存緩存,只要顯示在頁面上的,都會緩存在活動緩存。
Engin的waitForExistingOrStartNewJob方法
private <R> LoadStatus waitForExistingOrStartNewJob(
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,
boolean isScaleOnlyOrNoTransform,
Options options,
boolean isMemoryCacheable,
boolean useUnlimitedSourceExecutorPool,
boolean useAnimationPool,
boolean onlyRetrieveFromCache,
ResourceCallback cb,
Executor callbackExecutor,
EngineKey key,
long startTime) {
EngineJob<?> current = jobs.get(key, onlyRetrieveFromCache);
if (current != null) {
current.addCallback(cb, callbackExecutor);
if (VERBOSE_IS_LOGGABLE) {
logWithTimeAndKey("Added to existing load", startTime, key);
}
return new LoadStatus(cb, current);
}
EngineJob<R> engineJob = //這個是線程池的管理者這個里邊存著各個管理
engineJobFactory.build(
key,
isMemoryCacheable,
useUnlimitedSourceExecutorPool,
useAnimationPool,
onlyRetrieveFromCache);
DecodeJob<R> decodeJob = //這個是具體的任務(wù)
decodeJobFactory.build(
glideContext,
model,
key,
signature,
width,
height,
resourceClass,
transcodeClass,
priority,
diskCacheStrategy,
transformations,
isTransformationRequired,
isScaleOnlyOrNoTransform,
onlyRetrieveFromCache,
options,
engineJob);
jobs.put(key, engineJob);
engineJob.addCallback(cb, callbackExecutor);
engineJob.start(decodeJob);
if (VERBOSE_IS_LOGGABLE) {
logWithTimeAndKey("Started new load", startTime, key);
}
return new LoadStatus(cb, engineJob);
}engineJob 管理一個線程池,decodeJob這個是任務(wù)的抽象是一個Runable,所以下面就會執(zhí)行任務(wù)的run方法
DecodeJob的run方法
public void run() {
...
try {
if (isCancelled) {
notifyFailed();
return;
}
runWrapped();
} catch (CallbackException e) {
// If a callback not controlled by Glide throws an exception, we should avoid the Glide
// specific debug logic below.
throw e;
} catch (Throwable t) {
..
} finally {
// Keeping track of the fetcher here and calling cleanup is excessively paranoid, we call
// close in all cases anyway.
if (localFetcher != null) {
localFetcher.cleanup();
}
GlideTrace.endSection();
}
}
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);
}
}
默認會執(zhí)行case INITIALIZE的runGenerators(),getNextGenerator得到的是SourceGenerator,這個必須要明確,否則走不下去。
SourceGenerator的startNext方法
public boolean startNext() {
...
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;
startNextLoad(loadData);
}
}
return started;
}
DecodeHelper的getLoadData方法
List<LoadData<?>> getLoadData() {
if (!isLoadDataSet) {
isLoadDataSet = true;
loadData.clear();
List<ModelLoader<Object, ?>> modelLoaders = glideContext.getRegistry().getModelLoaders(model);
//noinspection ForLoopReplaceableByForEach to improve perf
for (int i = 0, size = modelLoaders.size(); 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;
}
HttpGlideUrlLoader的buildLoadData方法
public LoadData<InputStream> buildLoadData(
@NonNull GlideUrl model, int width, int height, @NonNull Options options) {
// GlideUrls memoize parsed URLs so caching them saves a few object instantiations and time
// spent parsing urls.
GlideUrl url = model;
if (modelCache != null) {
url = modelCache.get(model, 0, 0);
if (url == null) {
modelCache.put(model, 0, 0, model);
url = model;
}
}
int timeout = options.get(TIMEOUT);
return new LoadData<>(url, new HttpUrlFetcher(url, timeout));
}
可以看到最終返回的是包含HttpUrlFetcher的loadData
SourceGenerator的startNextLoad方法
private void startNextLoad(final LoadData<?> toStart) {
loadData.fetcher.loadData(
helper.getPriority(),
new DataCallback<Object>() {
@Override
public void onDataReady(@Nullable Object data) {
if (isCurrentRequest(toStart)) {
onDataReadyInternal(toStart, data);
}
}
@Override
public void onLoadFailed(@NonNull Exception e) {
if (isCurrentRequest(toStart)) {
onLoadFailedInternal(toStart, e);
}
}
});
}
loadData.fetcher的fetcher就是HttpUrlFetcher
HttpUrlFetcher的loadData方法
public void loadData(
@NonNull Priority priority, @NonNull DataCallback<? super InputStream> callback) {
long startTime = LogTime.getLogTime();
try {
InputStream result = loadDataWithRedirects(glideUrl.toURL(), 0, null, glideUrl.getHeaders());
callback.onDataReady(result);
} catch (IOException e) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Failed to load data for url", e);
}
callback.onLoadFailed(e);
} finally {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Finished http url fetcher fetch in " + LogTime.getElapsedMillis(startTime));
}
}
}
loadDataWithRedirects方法
private InputStream loadDataWithRedirects(
URL url, int redirects, URL lastUrl, Map<String, String> headers) throws HttpException {
if (redirects >= MAXIMUM_REDIRECTS) {
throw new HttpException(
"Too many (> " + MAXIMUM_REDIRECTS + ") redirects!", INVALID_STATUS_CODE);
} else {
// Comparing the URLs using .equals performs additional network I/O and is generally broken.
// See http://michaelscharf.blogspot.com/2006/11/javaneturlequals-and-hashcode-make.html.
try {
if (lastUrl != null && url.toURI().equals(lastUrl.toURI())) {
throw new HttpException("In re-direct loop", INVALID_STATUS_CODE);
}
} catch (URISyntaxException e) {
// Do nothing, this is best effort.
}
}
urlConnection = buildAndConfigureConnection(url, headers);
try {
// Connect explicitly to avoid errors in decoders if connection fails.
urlConnection.connect();
// Set the stream so that it's closed in cleanup to avoid resource leaks. See #2352.
stream = urlConnection.getInputStream();
} catch (IOException e) {
throw new HttpException(
"Failed to connect or obtain data", getHttpStatusCodeOrInvalid(urlConnection), e);
}
...
}
到這里就看到urlConnection拿到圖片的InputStream,終于從一個網(wǎng)絡(luò)鏈接請求到流了,然后就是將流轉(zhuǎn)成bitmap顯示的過程了,感興趣的可以繼續(xù)查看。
5.Glide
聲明周期和緩存總結(jié) Glide 調(diào)用with函數(shù)如果傳入的ApplicationContext或者在子線程不會為我們創(chuàng)建空白的Fragment,不會進行生命周期的監(jiān)控,也就不會進行資源自動釋放,所以不用傳ApplicationContext。其他情況glide會自動創(chuàng)建空白的Fragment來進行生命周期監(jiān)控,并自動進行資源釋放。
Glide的緩存分為4級緩存:活動緩存、內(nèi)存緩存、磁盤緩存、網(wǎng)絡(luò)緩存
- 活動緩存,是一個Map集合,加載圖片會首先從這個緩存里邊查找。
- 內(nèi)存緩存,是一個LRU緩存集合,如果獲取緩存沒有就從內(nèi)存緩存查找。
- 磁盤緩存,是disklrucache緩存集合,如果從內(nèi)存緩存沒有找到就從磁盤緩存查找。
- 網(wǎng)絡(luò)緩存,如果磁盤緩存沒有就需要從網(wǎng)絡(luò)上就行請求加載了。
到此這篇關(guān)于Android圖片加載框架Gilde源碼層深入分析的文章就介紹到這了,更多相關(guān)Android Gilde內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Android 提交或者上傳數(shù)據(jù)時的dialog彈框動畫效果
我們在使用支付寶支付的時候會看到類似這種彈框動畫效果,下面通過實例代碼給大家分享android 提交或者上傳數(shù)據(jù)時的彈框動畫效果,感興趣的的朋友參考下2017-07-07
Android中利用NetworkInfo判斷網(wǎng)絡(luò)狀態(tài)時出現(xiàn)空指針(NullPointerException)問題的解決
這篇文章主要介紹了Android中利用NetworkInfo判斷網(wǎng)絡(luò)狀態(tài)時出現(xiàn)空指針(NullPointerException)問題的解決方法,非常不錯,具有參考借鑒價值,需要的朋友可以參考下2016-11-11
flutter中build.gradle倉庫的配置(解決外網(wǎng)下載速度過慢失敗的問題)
這篇文章主要介紹了flutter中build.gradle倉庫的配置,解決外網(wǎng)下載速度過慢,失敗的問題,本文通過圖文并茂的形式給大家介紹的非常詳細,具有一定的參考借鑒價值,需要的朋友可以參考下2020-05-05
Android convinientbanner頂部廣告輪播控件使用詳解
這篇文章主要為大家詳細介紹了Android convinientbanner頂部廣告輪播控件,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-01-01

