Android AsyncTask完全解析 帶你從源碼的角度徹底理解
我們都知道,Android UI是線程不安全的,如果想要在子線程里進(jìn)行UI操作,就需要借助Android的異步消息處理機(jī)制。之前我也寫過了一篇文章從源碼層面分析了Android的異步消息處理機(jī)制。
不過為了更加方便我們?cè)谧泳€程中更新UI元素,Android從1.5版本就引入了一個(gè)AsyncTask類,使用它就可以非常靈活方便地從子線程切換到UI線程,我們本篇文章的主角也就正是它了。
AsyncTask很早就出現(xiàn)在Android的API里了,所以我相信大多數(shù)朋友對(duì)它的用法都已經(jīng)非常熟悉。不過今天我還是準(zhǔn)備從AsyncTask的基本用法開始講起,然后我們?cè)賮硪黄鸱治鱿翧syncTask源碼,看看它是如何實(shí)現(xiàn)的,最后我會(huì)介紹一些關(guān)于AsyncTask你所不知道的秘密。
AsyncTask的基本用法
首先來看一下AsyncTask的基本用法,由于AsyncTask是一個(gè)抽象類,所以如果我們想使用它,就必須要?jiǎng)?chuàng)建一個(gè)子類去繼承它。在繼承時(shí)我們可以為AsyncTask類指定三個(gè)泛型參數(shù),這三個(gè)參數(shù)的用途如下:
1. Params
在執(zhí)行AsyncTask時(shí)需要傳入的參數(shù),可用于在后臺(tái)任務(wù)中使用。
2. Progress
后臺(tái)任務(wù)執(zhí)行時(shí),如果需要在界面上顯示當(dāng)前的進(jìn)度,則使用這里指定的泛型作為進(jìn)度單位。
3. Result
當(dāng)任務(wù)執(zhí)行完畢后,如果需要對(duì)結(jié)果進(jìn)行返回,則使用這里指定的泛型作為返回值類型。
因此,一個(gè)最簡(jiǎn)單的自定義AsyncTask就可以寫成如下方式:
class DownloadTask extends AsyncTask<Void, Integer, Boolean> { …… }
這里我們把AsyncTask的第一個(gè)泛型參數(shù)指定為Void,表示在執(zhí)行AsyncTask的時(shí)候不需要傳入?yún)?shù)給后臺(tái)任務(wù)。第二個(gè)泛型參數(shù)指定為Integer,表示使用整型數(shù)據(jù)來作為進(jìn)度顯示單位。第三個(gè)泛型參數(shù)指定為Boolean,則表示使用布爾型數(shù)據(jù)來反饋執(zhí)行結(jié)果。
當(dāng)然,目前我們自定義的DownloadTask還是一個(gè)空任務(wù),并不能進(jìn)行任何實(shí)際的操作,我們還需要去重寫AsyncTask中的幾個(gè)方法才能完成對(duì)任務(wù)的定制。經(jīng)常需要去重寫的方法有以下四個(gè):
1. onPreExecute()
這個(gè)方法會(huì)在后臺(tái)任務(wù)開始執(zhí)行之間調(diào)用,用于進(jìn)行一些界面上的初始化操作,比如顯示一個(gè)進(jìn)度條對(duì)話框等。
2. doInBackground(Params...)
這個(gè)方法中的所有代碼都會(huì)在子線程中運(yùn)行,我們應(yīng)該在這里去處理所有的耗時(shí)任務(wù)。任務(wù)一旦完成就可以通過return語句來將任務(wù)的執(zhí)行結(jié)果進(jìn)行返回,如果AsyncTask的第三個(gè)泛型參數(shù)指定的是Void,就可以不返回任務(wù)執(zhí)行結(jié)果。注意,在這個(gè)方法中是不可以進(jìn)行UI操作的,如果需要更新UI元素,比如說反饋當(dāng)前任務(wù)的執(zhí)行進(jìn)度,可以調(diào)用publishProgress(Progress...)方法來完成。
3. onProgressUpdate(Progress...)
當(dāng)在后臺(tái)任務(wù)中調(diào)用了publishProgress(Progress...)方法后,這個(gè)方法就很快會(huì)被調(diào)用,方法中攜帶的參數(shù)就是在后臺(tái)任務(wù)中傳遞過來的。在這個(gè)方法中可以對(duì)UI進(jìn)行操作,利用參數(shù)中的數(shù)值就可以對(duì)界面元素進(jìn)行相應(yīng)的更新。
4. onPostExecute(Result)
當(dāng)后臺(tái)任務(wù)執(zhí)行完畢并通過return語句進(jìn)行返回時(shí),這個(gè)方法就很快會(huì)被調(diào)用。返回的數(shù)據(jù)會(huì)作為參數(shù)傳遞到此方法中,可以利用返回的數(shù)據(jù)來進(jìn)行一些UI操作,比如說提醒任務(wù)執(zhí)行的結(jié)果,以及關(guān)閉掉進(jìn)度條對(duì)話框等。
因此,一個(gè)比較完整的自定義AsyncTask就可以寫成如下方式:
class DownloadTask extends AsyncTask<Void, Integer, Boolean> { @Override protected void onPreExecute() { progressDialog.show(); } @Override protected Boolean doInBackground(Void... params) { try { while (true) { int downloadPercent = doDownload(); publishProgress(downloadPercent); if (downloadPercent >= 100) { break; } } } catch (Exception e) { return false; } return true; } @Override protected void onProgressUpdate(Integer... values) { progressDialog.setMessage("當(dāng)前下載進(jìn)度:" + values[0] + "%"); } @Override protected void onPostExecute(Boolean result) { progressDialog.dismiss(); if (result) { Toast.makeText(context, "下載成功", Toast.LENGTH_SHORT).show(); } else { Toast.makeText(context, "下載失敗", Toast.LENGTH_SHORT).show(); } } }
這里我們模擬了一個(gè)下載任務(wù),在doInBackground()方法中去執(zhí)行具體的下載邏輯,在onProgressUpdate()方法中顯示當(dāng)前的下載進(jìn)度,在onPostExecute()方法中來提示任務(wù)的執(zhí)行結(jié)果。如果想要啟動(dòng)這個(gè)任務(wù),只需要簡(jiǎn)單地調(diào)用以下代碼即可:
new DownloadTask().execute();
以上就是AsyncTask的基本用法,怎么樣,是不是感覺在子線程和UI線程之間進(jìn)行切換變得靈活了很多?我們并不需求去考慮什么異步消息處理機(jī)制,也不需要專門使用一個(gè)Handler來發(fā)送和接收消息,只需要調(diào)用一下publishProgress()方法就可以輕松地從子線程切換到UI線程了。
分析AsyncTask的源碼
雖然AsyncTask這么簡(jiǎn)單好用,但你知道它是怎樣實(shí)現(xiàn)的嗎?那么接下來,我們就來分析一下AsyncTask的源碼,對(duì)它的實(shí)現(xiàn)原理一探究竟。注意這里我選用的是Android 4.0的源碼,如果你查看的是其它版本的源碼,可能會(huì)有一些出入。
從之前DownloadTask的代碼就可以看出,在啟動(dòng)某一個(gè)任務(wù)之前,要先new出它的實(shí)例,因此,我們就先來看一看AsyncTask構(gòu)造函數(shù)中的源碼,如下所示:
public AsyncTask() { mWorker = new WorkerRunnable<Params, Result>() { public Result call() throws Exception { mTaskInvoked.set(true); Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); return postResult(doInBackground(mParams)); } }; mFuture = new FutureTask<Result>(mWorker) { @Override protected void done() { try { final Result result = get(); postResultIfNotInvoked(result); } catch (InterruptedException e) { android.util.Log.w(LOG_TAG, e); } catch (ExecutionException e) { throw new RuntimeException("An error occured while executing doInBackground()", e.getCause()); } catch (CancellationException e) { postResultIfNotInvoked(null); } catch (Throwable t) { throw new RuntimeException("An error occured while executing " + "doInBackground()", t); } } }; }
這段代碼雖然看起來有點(diǎn)長(zhǎng),但實(shí)際上并沒有任何具體的邏輯會(huì)得到執(zhí)行,只是初始化了兩個(gè)變量,mWorker和mFuture,并在初始化mFuture的時(shí)候?qū)Worker作為參數(shù)傳入。mWorker是一個(gè)Callable對(duì)象,mFuture是一個(gè)FutureTask對(duì)象,這兩個(gè)變量會(huì)暫時(shí)保存在內(nèi)存中,稍后才會(huì)用到它們。
接著如果想要啟動(dòng)某一個(gè)任務(wù),就需要調(diào)用該任務(wù)的execute()方法,因此現(xiàn)在我們來看一看execute()方法的源碼,如下所示:
public final AsyncTask<Params, Progress, Result> execute(Params... params) { return executeOnExecutor(sDefaultExecutor, params); }
簡(jiǎn)單的有點(diǎn)過分了,只有一行代碼,僅是調(diào)用了executeOnExecutor()方法,那么具體的邏輯就應(yīng)該寫在這個(gè)方法里了,快跟進(jìn)去瞧一瞧:
public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec, Params... params) { if (mStatus != Status.PENDING) { switch (mStatus) { case RUNNING: throw new IllegalStateException("Cannot execute task:" + " the task is already running."); case FINISHED: throw new IllegalStateException("Cannot execute task:" + " the task has already been executed " + "(a task can be executed only once)"); } } mStatus = Status.RUNNING; onPreExecute(); mWorker.mParams = params; exec.execute(mFuture); return this; }
果然,這里的代碼看上去才正常點(diǎn)??梢钥吹?,在第15行調(diào)用了onPreExecute()方法,因此證明了onPreExecute()方法會(huì)第一個(gè)得到執(zhí)行??墒墙酉聛淼拇a就看不明白了,怎么沒見到哪里有調(diào)用doInBackground()方法呢?別著急,慢慢找總會(huì)找到的,我們看到,在第17行調(diào)用了Executor的execute()方法,并將前面初始化的mFuture對(duì)象傳了進(jìn)去,那么這個(gè)Executor對(duì)象又是什么呢?查看上面的execute()方法,原來是傳入了一個(gè)sDefaultExecutor變量,接著找一下這個(gè)sDefaultExecutor變量是在哪里定義的,源碼如下所示:
public static final Executor SERIAL_EXECUTOR = new SerialExecutor(); …… private static volatile Executor sDefaultExecutor = SERIAL_EXECUTOR;
可以看到,這里先new出了一個(gè)SERIAL_EXECUTOR常量,然后將sDefaultExecutor的值賦值為這個(gè)常量,也就是說明,剛才在executeOnExecutor()方法中調(diào)用的execute()方法,其實(shí)也就是調(diào)用的SerialExecutor類中的execute()方法。那么我們自然要去看看SerialExecutor的源碼了,如下所示:
private static class SerialExecutor implements Executor { final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>(); Runnable mActive; public synchronized void execute(final Runnable r) { mTasks.offer(new Runnable() { public void run() { try { r.run(); } finally { scheduleNext(); } } }); if (mActive == null) { scheduleNext(); } } protected synchronized void scheduleNext() { if ((mActive = mTasks.poll()) != null) { THREAD_POOL_EXECUTOR.execute(mActive); } } }
SerialExecutor類中也有一個(gè)execute()方法,這個(gè)方法里的所有邏輯就是在子線程中執(zhí)行的了,注意這個(gè)方法有一個(gè)Runnable參數(shù),那么目前這個(gè)參數(shù)的值是什么呢?當(dāng)然就是mFuture對(duì)象了,也就是說在第9行我們要調(diào)用的是FutureTask類的run()方法,而在這個(gè)方法里又會(huì)去調(diào)用Sync內(nèi)部類的innerRun()方法,因此我們直接來看innerRun()方法的源碼:
void innerRun() { if (!compareAndSetState(READY, RUNNING)) return; runner = Thread.currentThread(); if (getState() == RUNNING) { // recheck after setting thread V result; try { result = callable.call(); } catch (Throwable ex) { setException(ex); return; } set(result); } else { releaseShared(0); // cancel } }
可以看到,在第8行調(diào)用了callable的call()方法,那么這個(gè)callable對(duì)象是什么呢?其實(shí)就是在初始化mFuture對(duì)象時(shí)傳入的mWorker對(duì)象了,此時(shí)調(diào)用的call()方法,也就是一開始在AsyncTask的構(gòu)造函數(shù)中指定的,我們把它單獨(dú)拿出來看一下,代碼如下所示:
public Result call() throws Exception { mTaskInvoked.set(true); Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); return postResult(doInBackground(mParams)); }
在postResult()方法的參數(shù)里面,我們終于找到了doInBackground()方法的調(diào)用處,雖然經(jīng)過了很多周轉(zhuǎn),但目前的代碼仍然是運(yùn)行在子線程當(dāng)中的,所以這也就是為什么我們可以在doInBackground()方法中去處理耗時(shí)的邏輯。接著將doInBackground()方法返回的結(jié)果傳遞給了postResult()方法,這個(gè)方法的源碼如下所示:
private Result postResult(Result result) { Message message = sHandler.obtainMessage(MESSAGE_POST_RESULT, new AsyncTaskResult<Result>(this, result)); message.sendToTarget(); return result; }
如果你已經(jīng)熟悉了異步消息處理機(jī)制,這段代碼對(duì)你來說一定非常簡(jiǎn)單吧。這里使用sHandler對(duì)象發(fā)出了一條消息,消息中攜帶了MESSAGE_POST_RESULT常量和一個(gè)表示任務(wù)執(zhí)行結(jié)果的AsyncTaskResult對(duì)象。這個(gè)sHandler對(duì)象是InternalHandler類的一個(gè)實(shí)例,那么稍后這條消息肯定會(huì)在InternalHandler的handleMessage()方法中被處理。InternalHandler的源碼如下所示:
private static class InternalHandler extends Handler { @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"}) @Override public void handleMessage(Message msg) { AsyncTaskResult result = (AsyncTaskResult) msg.obj; switch (msg.what) { case MESSAGE_POST_RESULT: // There is only one result result.mTask.finish(result.mData[0]); break; case MESSAGE_POST_PROGRESS: result.mTask.onProgressUpdate(result.mData); break; } } }
這里對(duì)消息的類型進(jìn)行了判斷,如果這是一條MESSAGE_POST_RESULT消息,就會(huì)去執(zhí)行finish()方法,如果這是一條MESSAGE_POST_PROGRESS消息,就會(huì)去執(zhí)行onProgressUpdate()方法。那么finish()方法的源碼如下所示:
private void finish(Result result) { if (isCancelled()) { onCancelled(result); } else { onPostExecute(result); } mStatus = Status.FINISHED; }
可以看到,如果當(dāng)前任務(wù)被取消掉了,就會(huì)調(diào)用onCancelled()方法,如果沒有被取消,則調(diào)用onPostExecute()方法,這樣當(dāng)前任務(wù)的執(zhí)行就全部結(jié)束了。
我們注意到,在剛才InternalHandler的handleMessage()方法里,還有一種MESSAGE_POST_PROGRESS的消息類型,這種消息是用于當(dāng)前進(jìn)度的,調(diào)用的正是onProgressUpdate()方法,那么什么時(shí)候才會(huì)發(fā)出這樣一條消息呢?相信你已經(jīng)猜到了,查看publishProgress()方法的源碼,如下所示:
protected final void publishProgress(Progress... values) { if (!isCancelled()) { sHandler.obtainMessage(MESSAGE_POST_PROGRESS, new AsyncTaskResult<Progress>(this, values)).sendToTarget(); } }
非常清晰了吧!正因如此,在doInBackground()方法中調(diào)用publishProgress()方法才可以從子線程切換到UI線程,從而完成對(duì)UI元素的更新操作。其實(shí)也沒有什么神秘的,因?yàn)檎f到底,AsyncTask也是使用的異步消息處理機(jī)制,只是做了非常好的封裝而已。
讀到這里,相信你對(duì)AsyncTask中的每個(gè)回調(diào)方法的作用、原理、以及何時(shí)會(huì)被調(diào)用都已經(jīng)搞明白了吧。
關(guān)于AsyncTask你所不知道的秘密
不得不說,剛才我們?cè)诜治鯯erialExecutor的時(shí)候,其實(shí)并沒有分析的很仔細(xì),僅僅只是關(guān)注了它會(huì)調(diào)用mFuture中的run()方法,但是至于什么時(shí)候會(huì)調(diào)用我們并沒有進(jìn)一步地研究。其實(shí)SerialExecutor也是AsyncTask在3.0版本以后做了最主要的修改的地方,它在AsyncTask中是以常量的形式被使用的,因此在整個(gè)應(yīng)用程序中的所有AsyncTask實(shí)例都會(huì)共用同一個(gè)SerialExecutor。下面我們就來對(duì)這個(gè)類進(jìn)行更加詳細(xì)的分析,為了方便閱讀,我把它的代碼再貼出來一遍:
private static class SerialExecutor implements Executor { final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>(); Runnable mActive; public synchronized void execute(final Runnable r) { mTasks.offer(new Runnable() { public void run() { try { r.run(); } finally { scheduleNext(); } } }); if (mActive == null) { scheduleNext(); } } protected synchronized void scheduleNext() { if ((mActive = mTasks.poll()) != null) { THREAD_POOL_EXECUTOR.execute(mActive); } } }
可以看到,SerialExecutor是使用ArrayDeque這個(gè)隊(duì)列來管理Runnable對(duì)象的,如果我們一次性啟動(dòng)了很多個(gè)任務(wù),首先在第一次運(yùn)行execute()方法的時(shí)候,會(huì)調(diào)用ArrayDeque的offer()方法將傳入的Runnable對(duì)象添加到隊(duì)列的尾部,然后判斷mActive對(duì)象是不是等于null,第一次運(yùn)行當(dāng)然是等于null了,于是會(huì)調(diào)用scheduleNext()方法。在這個(gè)方法中會(huì)從隊(duì)列的頭部取值,并賦值給mActive對(duì)象,然后調(diào)用THREAD_POOL_EXECUTOR去執(zhí)行取出的取出的Runnable對(duì)象。之后如何又有新的任務(wù)被執(zhí)行,同樣還會(huì)調(diào)用offer()方法將傳入的Runnable添加到隊(duì)列的尾部,但是再去給mActive對(duì)象做非空檢查的時(shí)候就會(huì)發(fā)現(xiàn)mActive對(duì)象已經(jīng)不再是null了,于是就不會(huì)再調(diào)用scheduleNext()方法。
那么后面添加的任務(wù)豈不是永遠(yuǎn)得不到處理了?當(dāng)然不是,看一看offer()方法里傳入的Runnable匿名類,這里使用了一個(gè)try finally代碼塊,并在finally中調(diào)用了scheduleNext()方法,保證無論發(fā)生什么情況,這個(gè)方法都會(huì)被調(diào)用。也就是說,每次當(dāng)一個(gè)任務(wù)執(zhí)行完畢后,下一個(gè)任務(wù)才會(huì)得到執(zhí)行,SerialExecutor模仿的是單一線程池的效果,如果我們快速地啟動(dòng)了很多任務(wù),同一時(shí)刻只會(huì)有一個(gè)線程正在執(zhí)行,其余的均處于等待狀態(tài)。Android照片墻應(yīng)用實(shí)現(xiàn),再多的圖片也不怕崩潰 這篇文章中例子的運(yùn)行結(jié)果也證實(shí)了這個(gè)結(jié)論。
不過你可能還不知道,在Android 3.0之前是并沒有SerialExecutor這個(gè)類的,那個(gè)時(shí)候是直接在AsyncTask中構(gòu)建了一個(gè)sExecutor常量,并對(duì)線程池總大小,同一時(shí)刻能夠運(yùn)行的線程數(shù)做了規(guī)定,代碼如下所示:
private static final int CORE_POOL_SIZE = 5; private static final int MAXIMUM_POOL_SIZE = 128; private static final int KEEP_ALIVE = 10; …… private static final ThreadPoolExecutor sExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE,
MAXIMUM_POOL_SIZE, KEEP_ALIVE, TimeUnit.SECONDS, sWorkQueue, sThreadFactory);
可以看到,這里規(guī)定同一時(shí)刻能夠運(yùn)行的線程數(shù)為5個(gè),線程池總大小為128。也就是說當(dāng)我們啟動(dòng)了10個(gè)任務(wù)時(shí),只有5個(gè)任務(wù)能夠立刻執(zhí)行,另外的5個(gè)任務(wù)則需要等待,當(dāng)有一個(gè)任務(wù)執(zhí)行完畢后,第6個(gè)任務(wù)才會(huì)啟動(dòng),以此類推。而線程池中最大能存放的線程數(shù)是128個(gè),當(dāng)我們嘗試去添加第129個(gè)任務(wù)時(shí),程序就會(huì)崩潰。
因此在3.0版本中AsyncTask的改動(dòng)還是挺大的,在3.0之前的AsyncTask可以同時(shí)有5個(gè)任務(wù)在執(zhí)行,而3.0之后的AsyncTask同時(shí)只能有1個(gè)任務(wù)在執(zhí)行。為什么升級(jí)之后可以同時(shí)執(zhí)行的任務(wù)數(shù)反而變少了呢?這是因?yàn)楦潞蟮腁syncTask已變得更加靈活,如果不想使用默認(rèn)的線程池,還可以自由地進(jìn)行配置。比如使用如下的代碼來啟動(dòng)任務(wù):
Executor exec = new ThreadPoolExecutor(15, 200, 10, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>()); new DownloadTask().executeOnExecutor(exec);
這樣就可以使用我們自定義的一個(gè)Executor來執(zhí)行任務(wù),而不是使用SerialExecutor。上述代碼的效果允許在同一時(shí)刻有15個(gè)任務(wù)正在執(zhí)行,并且最多能夠存儲(chǔ)200個(gè)任務(wù)。
好了,到這里我們就已經(jīng)把關(guān)于AsyncTask的所有重要內(nèi)容深入淺出地理解了一遍,相信在將來使用它的時(shí)候能夠更加得心應(yīng)手。
相關(guān)文章
Android通過Java sdk的方式接入OpenCv的方法
這篇文章主要介紹了Android通過Java sdk的方式接入OpenCv的方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-04-04Android實(shí)現(xiàn)沉浸式狀態(tài)欄功能
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)沉浸式狀態(tài)欄功能,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-10-10android自定義進(jìn)度條漸變色View的實(shí)例代碼
這篇文章主要介紹了android自定義進(jìn)度條漸變色View的實(shí)例代碼,有需要的朋友可以參考一下2014-01-01android照相、相冊(cè)獲取圖片剪裁報(bào)錯(cuò)的解決方法
最近在項(xiàng)目中用到了照相和相冊(cè)取圖剪裁上傳頭像,就在網(wǎng)上逛了逛,基本都是千篇一律,就弄下來用了用,沒想到的是各種各樣的奇葩問題就出現(xiàn)了。先給大家看看代碼問題慢慢來解決2014-11-11Android編程向服務(wù)器發(fā)送請(qǐng)求時(shí)出現(xiàn)中文亂碼問題的解決方法
這篇文章主要介紹了Android編程向服務(wù)器發(fā)送請(qǐng)求時(shí)出現(xiàn)中文亂碼問題的解決方法,實(shí)例分析了Android參數(shù)傳遞過程中中文亂碼的解決技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-11-11Android編程實(shí)現(xiàn)兩點(diǎn)觸控功能示例
這篇文章主要介紹了Android編程實(shí)現(xiàn)兩點(diǎn)觸控功能的方法,涉及Android事件響應(yīng)與處理相關(guān)操作技巧,需要的朋友可以參考下2017-08-08Android源碼導(dǎo)入AndroidStudio或IntelliJ?IDEA的方法
這篇文章主要介紹了Android源碼導(dǎo)入AndroidStudio或IntelliJ?IDEA的方法,用idegen來生成針對(duì)AndroidStudio或IntelliJ?IDEA的Android系統(tǒng)源代碼工程配置文件,需要的朋友可以參考下2022-08-08Android 加載大圖、多圖和LruCache緩存詳細(xì)介紹
這篇文章主要介紹了Android 加載大圖、多圖和LruCache緩存詳細(xì)介紹的相關(guān)資料,需要的朋友可以參考下2016-10-10Android SQLite數(shù)據(jù)庫(kù)操作代碼類分享
這篇文章主要介紹了Android SQLite數(shù)據(jù)庫(kù)操作代碼類分享,本文直接給出實(shí)現(xiàn)代碼和使用代碼,需要的朋友可以參考下2015-03-03