分析Android中應(yīng)用的啟動(dòng)流程
前言
在我們開(kāi)始之前,希望您能最好已經(jīng)滿足以下條件:
1、有一份編譯后的Android源碼(親自動(dòng)手實(shí)踐才會(huì)有更深入的理解)
2、對(duì)Binder機(jī)制有一定的了解
本文啟動(dòng)流程分析基于Android 5.1的源碼。為什么是5.1的源碼呢?因?yàn)槭诌吘幾g完的代碼只有這個(gè)版本…另外,用什么版本的源碼并不重要,大體的流程并無(wú)本質(zhì)上的區(qū)別,僅僅是實(shí)現(xiàn)細(xì)節(jié)的調(diào)整,找一個(gè)你熟悉的版本就好。
1、啟動(dòng)時(shí)序圖
作為一個(gè)輕微強(qiáng)迫癥的人,整理的時(shí)序圖,相信大家按圖索驥,一定能搞明白整個(gè)啟動(dòng)流程:
說(shuō)明:為了讓大家更清楚的理解整個(gè)過(guò)程,將時(shí)序圖中劃分為三個(gè)部分:Launcher進(jìn)程、System進(jìn)程、App進(jìn)程,其中有涉及共用的類以L / A進(jìn)行區(qū)分表示跟哪個(gè)進(jìn)程有關(guān),便于理解。
2、關(guān)鍵類說(shuō)明
整個(gè)啟動(dòng)流程因?yàn)闀?huì)涉及到多次Binder通信,這里先簡(jiǎn)要說(shuō)明一下幾個(gè)類的用途,方便大家理解整個(gè)交互流程:
1、ActivityManagerService:AMS是Android中最核心的服務(wù)之一,主要負(fù)責(zé)系統(tǒng)中四大組件的啟動(dòng)、切換、調(diào)度及應(yīng)用進(jìn)程的管理和調(diào)度等工作,其職責(zé)與操作系統(tǒng)中的進(jìn)程管理和調(diào)度模塊相類似,因此它在Android中非常重要,它本身也是一個(gè)Binder的實(shí)現(xiàn)類。
2、Instrumentation:顧名思義,它用來(lái)監(jiān)控應(yīng)用程序和系統(tǒng)的交互。
3、ActivityThread:應(yīng)用的入口類,系統(tǒng)通過(guò)調(diào)用main函數(shù),開(kāi)啟消息循環(huán)隊(duì)列。ActivityThread所在線程被稱為應(yīng)用的主線程(UI線程)。
4、ApplicationThread:ApplicationThread提供Binder通訊接口,AMS則通過(guò)代理調(diào)用此App進(jìn)程的本地方法。
5、ActivityManagerProxy:AMS服務(wù)在當(dāng)前進(jìn)程的代理類,負(fù)責(zé)與AMS通信。
6、ApplicationThreadProxy:ApplicationThread在AMS服務(wù)中的代理類,負(fù)責(zé)與ApplicationThread通信。
3、流程分析
首先交代下整個(gè)流程分析的場(chǎng)景:用戶點(diǎn)擊Launcher上的應(yīng)用圖標(biāo)到該應(yīng)用主界面啟動(dòng)展示在用戶眼前。
這整個(gè)過(guò)程涉及到跨進(jìn)程通信,所以我們將其劃分為時(shí)序圖中所展示三個(gè)進(jìn)程:Launcher進(jìn)程、System進(jìn)程、App進(jìn)程。為了不貼過(guò)長(zhǎng)的代碼又能說(shuō)清楚進(jìn)程間交互的流程,這里簡(jiǎn)述幾個(gè)重要的交互點(diǎn)。
從時(shí)序圖上大家也可以看到調(diào)用鏈相當(dāng)長(zhǎng),對(duì)應(yīng)的代碼量也比較大,而且時(shí)序圖只是分析了這個(gè)一個(gè)場(chǎng)景下的流程。道阻且長(zhǎng),行則將至!
3.1 Launcher響應(yīng)用戶點(diǎn)擊,通知AMS
Launcher做為應(yīng)用的入口,還是有必要交代一下的,我們來(lái)看看Launcher的代碼片段,Launcher使用的是packages/apps/Launcher3的的源碼。
public class Launcher extends Activity implements View.OnClickListener, OnLongClickListener, LauncherModel.Callbacks, View.OnTouchListener, PageSwitchListener, LauncherProviderChangeListener { ... /** * Launches the intent referred by the clicked shortcut. * * @param v The view representing the clicked shortcut. */ public void onClick(View v) { // Make sure that rogue clicks don't get through while allapps is launching, or after the // view has detached (it's possible for this to happen if the view is removed mid touch). if (v.getWindowToken() == null) { return; } ... Object tag = v.getTag(); if (tag instanceof ShortcutInfo) { onClickAppShortcut(v); } else if (tag instanceof FolderInfo) { ... } else if (v == mAllAppsButton) { onClickAllAppsButton(v); } else if (tag instanceof AppInfo) { startAppShortcutOrInfoActivity(v); } else if (tag instanceof LauncherAppWidgetInfo) { ... } } private void startAppShortcutOrInfoActivity(View v) { ... boolean success = startActivitySafely(v, intent, tag); ... } boolean startActivitySafely(View v, Intent intent, Object tag) { ... try { success = startActivity(v, intent, tag); } catch (ActivityNotFoundException e) { ... } return success; } boolean startActivity(View v, Intent intent, Object tag) { intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); try { ... if (user == null || user.equals(UserHandleCompat.myUserHandle())) { // Could be launching some bookkeeping activity startActivity(intent, optsBundle); } else { ... } return true; } catch (SecurityException e) { ... } return false; } }
通過(guò)starActicity輾轉(zhuǎn)調(diào)用到Activity:startActivityForResult
而后則調(diào)用至Instrumentation:execStartActivity
,代碼片段如下:
public class Instrumentation { ... public ActivityResult execStartActivity( Context who, IBinder contextThread, IBinder token, Activity target, Intent intent, int requestCode, Bundle options) { IApplicationThread whoThread = (IApplicationThread) contextThread; ... try { ... int result = ActivityManagerNative.getDefault() .startActivity(whoThread, who.getBasePackageName(), intent, intent.resolveTypeIfNeeded(who.getContentResolver()), token, target != null ? target.mEmbeddedID : null, requestCode, 0, null, options); ... } catch (RemoteException e) { } return null; } ... }
這里的ActivityManagerNative.getDefault
返回ActivityManagerService
的遠(yuǎn)程接口,即ActivityManagerProxy
接口,有人可能會(huì)問(wèn)了為什么會(huì)是ActivityManagerProxy
,這就涉及到Binder通信了,這里不再展開(kāi)。通過(guò)Binder驅(qū)動(dòng)程序,ActivityManagerProxy
與AMS服務(wù)通信,則實(shí)現(xiàn)了跨進(jìn)程到System進(jìn)程。
3.2 AMS響應(yīng)Launcher進(jìn)程請(qǐng)求
從上面的流程我們知道,此時(shí)AMS應(yīng)該處理Launcher進(jìn)程發(fā)來(lái)的請(qǐng)求,請(qǐng)參看時(shí)序圖及源碼,此時(shí)我們來(lái)看ActivityStackSupervisor:startActivityUncheckedLocked
方法,目測(cè)這個(gè)方法已經(jīng)超過(guò)600行代碼,來(lái)看一些關(guān)鍵代碼片段:
public final class ActivityStackSupervisor implements DisplayListener { ... final int startActivityUncheckedLocked(ActivityRecord r, ActivityRecord sourceRecord, IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor, int startFlags, boolean doResume, Bundle options, TaskRecord inTask) { final Intent intent = r.intent; final int callingUid = r.launchedFromUid; ... final boolean launchSingleTop = r.launchMode == ActivityInfo.LAUNCH_SINGLE_TOP; final boolean launchSingleInstance = r.launchMode == ActivityInfo.LAUNCH_SINGLE_INSTANCE; final boolean launchSingleTask = r.launchMode == ActivityInfo.LAUNCH_SINGLE_TASK; int launchFlags = intent.getFlags(); ... // We'll invoke onUserLeaving before onPause only if the launching // activity did not explicitly state that this is an automated launch. mUserLeaving = (launchFlags & Intent.FLAG_ACTIVITY_NO_USER_ACTION) == 0; ... ActivityRecord notTop = (launchFlags & Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP) != 0 ? r : null; // If the onlyIfNeeded flag is set, then we can do this if the activity // being launched is the same as the one making the call... or, as // a special case, if we do not know the caller then we count the // current top activity as the caller. if ((startFlags&ActivityManager.START_FLAG_ONLY_IF_NEEDED) != 0) { ... } ... // If the caller is not coming from another activity, but has given us an // explicit task into which they would like us to launch the new activity, // then let's see about doing that. if (sourceRecord == null && inTask != null && inTask.stack != null) { final Intent baseIntent = inTask.getBaseIntent(); final ActivityRecord root = inTask.getRootActivity(); ... // If this task is empty, then we are adding the first activity -- it // determines the root, and must be launching as a NEW_TASK. if (launchSingleInstance || launchSingleTask) { ... } ... } ... if (inTask == null) { if (sourceRecord == null) { // This activity is not being started from another... in this // case we -always- start a new task. if ((launchFlags & Intent.FLAG_ACTIVITY_NEW_TASK) == 0 && inTask == null) { Slog.w(TAG, "startActivity called from non-Activity context; forcing " + "Intent.FLAG_ACTIVITY_NEW_TASK for: " + intent); launchFlags |= Intent.FLAG_ACTIVITY_NEW_TASK; } } else if (sourceRecord.launchMode == ActivityInfo.LAUNCH_SINGLE_INSTANCE) { // The original activity who is starting us is running as a single // instance... this new activity it is starting must go on its // own task. launchFlags |= Intent.FLAG_ACTIVITY_NEW_TASK; } else if (launchSingleInstance || launchSingleTask) { // The activity being started is a single instance... it always // gets launched into its own task. launchFlags |= Intent.FLAG_ACTIVITY_NEW_TASK; } } ... // We may want to try to place the new activity in to an existing task. We always // do this if the target activity is singleTask or singleInstance; we will also do // this if NEW_TASK has been requested, and there is not an additional qualifier telling // us to still place it in a new task: multi task, always doc mode, or being asked to // launch this as a new task behind the current one. if (((launchFlags & Intent.FLAG_ACTIVITY_NEW_TASK) != 0 && (launchFlags & Intent.FLAG_ACTIVITY_MULTIPLE_TASK) == 0) || launchSingleInstance || launchSingleTask) { // If bring to front is requested, and no result is requested and we have not // been given an explicit task to launch in to, and // we can find a task that was started with this same // component, then instead of launching bring that one to the front. if (inTask == null && r.resultTo == null) { // See if there is a task to bring to the front. If this is // a SINGLE_INSTANCE activity, there can be one and only one // instance of it in the history, and it is always in its own // unique task, so we do a special search. ActivityRecord intentActivity = !launchSingleInstance ? findTaskLocked(r) : findActivityLocked(intent, r.info); if (intentActivity != null) { ... } } } ... if (r.packageName != null) { // If the activity being launched is the same as the one currently // at the top, then we need to check if it should only be launched // once. ActivityStack topStack = getFocusedStack(); ActivityRecord top = topStack.topRunningNonDelayedActivityLocked(notTop); if (top != null && r.resultTo == null) { if (top.realActivity.equals(r.realActivity) && top.userId == r.userId) { ... } } } else{ ... } boolean newTask = false; boolean keepCurTransition = false; TaskRecord taskToAffiliate = launchTaskBehind && sourceRecord != null ? sourceRecord.task : null; // Should this be considered a new task? if (r.resultTo == null && inTask == null && !addingToTask && (launchFlags & Intent.FLAG_ACTIVITY_NEW_TASK) != 0) { ... if (reuseTask == null) { r.setTask(targetStack.createTaskRecord(getNextTaskId(), newTaskInfo != null ? newTaskInfo : r.info, newTaskIntent != null ? newTaskIntent : intent, voiceSession, voiceInteractor, !launchTaskBehind /* toTop */), taskToAffiliate); ... } else { r.setTask(reuseTask, taskToAffiliate); } ... } else if (sourceRecord != null) { } else if (!addingToTask && (launchFlags&Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) != 0) { } else if (inTask != null){ } else { } ... targetStack.startActivityLocked(r, newTask, doResume, keepCurTransition, options); ... return ActivityManager.START_SUCCESS; } ... }
函數(shù)經(jīng)過(guò)intent的標(biāo)志值設(shè)置,通過(guò)findTaskLocked
函數(shù)來(lái)查找存不存這樣的Task,這里返回的結(jié)果是null,即intentActivity
為null,因此,需要?jiǎng)?chuàng)建一個(gè)新的Task來(lái)啟動(dòng)這個(gè)Activity
?,F(xiàn)在處理堆棧頂端的Activity
是Launcher
,與我們即將要啟動(dòng)的MainActivity
不是同一個(gè)Activity
,創(chuàng)建了一個(gè)新的Task里面來(lái)啟動(dòng)這個(gè)Activity
。
經(jīng)過(guò)棧頂檢測(cè),則需要將Launcher推入Paused狀態(tài),才可以啟動(dòng)新的Activity
。后續(xù)則調(diào)用至ActivityStack:startPausingLocked
,我們來(lái)看一下這個(gè)函數(shù):
final class ActivityStack { ... final boolean startPausingLocked(boolean userLeaving, boolean uiSleeping, boolean resuming, boolean dontWait) { if (mPausingActivity != null) { ... } ActivityRecord prev = mResumedActivity; if (prev == null) { ... } ... mResumedActivity = null; mPausingActivity = prev; mLastPausedActivity = prev; mLastNoHistoryActivity = (prev.intent.getFlags() & Intent.FLAG_ACTIVITY_NO_HISTORY) != 0 || (prev.info.flags & ActivityInfo.FLAG_NO_HISTORY) != 0 ? prev : null; prev.state = ActivityState.PAUSING; ... if (prev.app != null && prev.app.thread != null) { try { ... prev.app.thread.schedulePauseActivity(prev.appToken, prev.finishing, userLeaving, prev.configChangeFlags, dontWait); } catch (Exception e) { ... } } else { ... } ... } ... }
這里的prev.app.thread
是一個(gè)ApplicationThread
對(duì)象的遠(yuǎn)程接口,通過(guò)調(diào)用這個(gè)遠(yuǎn)程接口的schedulePauseActivity
來(lái)通知Launcher進(jìn)入Paused狀態(tài)。至此,AMS對(duì)Launcher的請(qǐng)求已經(jīng)響應(yīng),這是我們發(fā)現(xiàn)又通過(guò)Binder通信回調(diào)至Launcher進(jìn)程。
3.3 Launcher進(jìn)程掛起Launcher,再次通知AMS
這個(gè)流程相對(duì)會(huì)簡(jiǎn)單一些,我們來(lái)看ActivityThread
:
public final class ActivityThread { ... private void handlePauseActivity(IBinder token, boolean finished, boolean userLeaving, int configChanges, boolean dontReport) { ActivityClientRecord r = mActivities.get(token); if (r != null) { ... performPauseActivity(token, finished, r.isPreHoneycomb()); // Make sure any pending writes are now committed. if (r.isPreHoneycomb()) { QueuedWork.waitToFinish(); } // Tell the activity manager we have paused. if (!dontReport) { try { ActivityManagerNative.getDefault().activityPaused(token); } catch (RemoteException ex) { } } ... } } ... }
這部分Launcher的ActivityThread
處理頁(yè)面Paused并且再次通過(guò)ActivityManagerProxy
通知AMS。
3.4 AMS創(chuàng)建新的進(jìn)程
創(chuàng)建新進(jìn)程的時(shí)候,AMS會(huì)保存一個(gè)ProcessRecord
信息,如果應(yīng)用程序中的AndroidManifest.xml配置文件中,我們沒(méi)有指定Application標(biāo)簽的process屬性,系統(tǒng)就會(huì)默認(rèn)使用package的名稱。每一個(gè)應(yīng)用程序都有自己的uid,因此,這里uid + process的組合就可以為每一個(gè)應(yīng)用程序創(chuàng)建一個(gè)ProcessRecord
。
public final class ActivityManagerService extends ActivityManagerNative implements Watchdog.Monitor, BatteryStatsImpl.BatteryCallback { ... private final void startProcessLocked(ProcessRecord app, String hostingType, String hostingNameStr, String abiOverride, String entryPoint, String[] entryPointArgs) { ... try { ... // Start the process. It will either succeed and return a result containing // the PID of the new process, or else throw a RuntimeException. boolean isActivityProcess = (entryPoint == null); if (entryPoint == null) entryPoint = "android.app.ActivityThread"; Process.ProcessStartResult startResult = Process.start(entryPoint, app.processName, uid, uid, gids, debugFlags, mountExternal, app.info.targetSdkVersion, app.info.seinfo, requiredAbi, instructionSet, app.info.dataDir, entryPointArgs); ... } catch () { ... } } ... }
這里主要是調(diào)用Process:start
接口來(lái)創(chuàng)建一個(gè)新的進(jìn)程,新的進(jìn)程會(huì)導(dǎo)入android.app.ActivityThread
類,并且執(zhí)行它的main
函數(shù),這就是每一個(gè)應(yīng)用程序都有一個(gè)ActivityThread
實(shí)例來(lái)對(duì)應(yīng)的原因。
3.5 應(yīng)用進(jìn)程初始化
我們來(lái)看Activity
的main
函數(shù),這里綁定了主線程的Looper,并進(jìn)入消息循環(huán),大家應(yīng)該知道,整個(gè)Android系統(tǒng)是消息驅(qū)動(dòng)的,這也是為什么主線程默認(rèn)綁定Looper的原因:
public final class ActivityThread { ... public static void main(String[] args) { ... Looper.prepareMainLooper(); ActivityThread thread = new ActivityThread(); thread.attach(false); ... Looper.loop(); ... } private void attach(boolean system) { ... if (!system) { ... final IActivityManager mgr = ActivityManagerNative.getDefault(); try { mgr.attachApplication(mAppThread); } catch (RemoteException ex) { // Ignore } } else { ... } ... } ... }
attach函數(shù)最終調(diào)用了ActivityManagerService
的遠(yuǎn)程接口ActivityManagerProxy的attachApplication
函數(shù),傳入的參數(shù)是mAppThread
,這是一個(gè)ApplicationThread
類型的Binder
對(duì)象,它的作用是AMS與應(yīng)用進(jìn)程進(jìn)行進(jìn)程間通信的。
3.6 在AMS中注冊(cè)應(yīng)用進(jìn)程,啟動(dòng)啟動(dòng)棧頂頁(yè)面
前面我們提到了AMS負(fù)責(zé)系統(tǒng)中四大組件的啟動(dòng)、切換、調(diào)度及應(yīng)用進(jìn)程的管理和調(diào)度等工作,通過(guò)上一個(gè)流程我們知道應(yīng)用進(jìn)程創(chuàng)建后通過(guò)Binder驅(qū)動(dòng)與AMS產(chǎn)生交互,此時(shí)AMS則將應(yīng)用進(jìn)程創(chuàng)建后的信息進(jìn)行了一次注冊(cè),如果拿Windows系統(tǒng)程序注冊(cè)到的注冊(cè)表來(lái)理解這個(gè)過(guò)程,可能會(huì)更形象一些。
mMainStack.topRunningActivityLocked(null)
從堆棧頂端取出要啟動(dòng)的Activity
,并在realStartActivityLockedhan
函數(shù)中通過(guò)ApplicationThreadProxy
調(diào)回App進(jìn)程啟動(dòng)頁(yè)面。
public final class ActivityStackSupervisor implements DisplayListener { ... final boolean realStartActivityLocked(ActivityRecord r, ProcessRecord app, boolean andResume, boolean checkConfig) throws RemoteException { ... r.app = app; ... try { ... app.thread.scheduleLaunchActivity(new Intent(r.intent), r.appToken, System.identityHashCode(r), r.info, new Configuration(mService.mConfiguration), r.compat, r.launchedFromPackage, r.task.voiceInteractor, app.repProcState, r.icicle, r.persistentState, results, newIntents, !andResume, mService.isNextTransitionForward(), profilerInfo); ... } catch (RemoteException e) { ... } ... } ... }
此時(shí)在App進(jìn)程,我們可以看到,經(jīng)過(guò)一些列的調(diào)用鏈最終調(diào)用至MainActivity:onCreate
函數(shù),之后會(huì)調(diào)用至onResume
,而后會(huì)通知AMS該MainActivity
已經(jīng)處于resume
狀態(tài)。至此,整個(gè)啟動(dòng)流程告一段落。
4、總結(jié)
通過(guò)上述流程,相信大家可以有了一個(gè)基本的認(rèn)知,這里我們忽略細(xì)節(jié)簡(jiǎn)化流程,單純從進(jìn)程角度來(lái)看下圖: launch_app_sim
圖上所畫(huà)這里就不在贅述,Activity啟動(dòng)后至Resume狀態(tài),此時(shí)可交互。以上就是分析Android中應(yīng)用啟動(dòng)流程的全部?jī)?nèi)容了,如何有疑問(wèn)歡迎大家指正交流。
- Android 啟動(dòng)activity的4種方式及打開(kāi)其他應(yīng)用的activity的坑
- Android應(yīng)用啟動(dòng)另外一個(gè)apk應(yīng)用的方法
- Android優(yōu)化應(yīng)用啟動(dòng)速度
- Android使用Intent啟動(dòng)其他非系統(tǒng)應(yīng)用程序的方法
- android應(yīng)用實(shí)現(xiàn)開(kāi)機(jī)自動(dòng)啟動(dòng)方法
- 解析android創(chuàng)建快捷方式會(huì)啟動(dòng)兩個(gè)應(yīng)用的問(wèn)題
- 解析Android應(yīng)用啟動(dòng)后自動(dòng)創(chuàng)建桌面快捷方式的實(shí)現(xiàn)方法
- Android筆記之:App應(yīng)用之啟動(dòng)界面SplashActivity的使用
- Android Intent啟動(dòng)別的應(yīng)用實(shí)現(xiàn)方法
- Android應(yīng)用框架之應(yīng)用啟動(dòng)過(guò)程詳解
相關(guān)文章
android 網(wǎng)絡(luò)編程之網(wǎng)絡(luò)通信幾種方式實(shí)例分享
這篇文章主要介紹了android 網(wǎng)絡(luò)編程之網(wǎng)絡(luò)通信幾種方式,有需要的朋友可以參考一下2013-12-12Android基準(zhǔn)配置文件Baseline?Profile方案提升啟動(dòng)速度
這篇文章主要為大家介紹了Android基準(zhǔn)配置文件Baseline?Profile方案提升啟動(dòng)速度示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-02-02Android實(shí)現(xiàn)讀取NFC卡的編號(hào)
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)讀取NFC卡的編號(hào),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-09-09使用科大訊飛語(yǔ)音SDK實(shí)現(xiàn)文字在線合成語(yǔ)音
這篇文章主要介紹了使用科大訊飛語(yǔ)音SDK實(shí)現(xiàn)文字在線合成語(yǔ)音 的相關(guān)資料,需要的朋友可以參考下2015-12-12詳解Android App中創(chuàng)建ViewPager組件的方法
這篇文章主要介紹了詳解Android App中創(chuàng)建ViewPager組件的方法,ViewPager最基本的功能就是可以使視圖滑動(dòng),需要的朋友可以參考下2016-03-03Android實(shí)現(xiàn)檢測(cè)手機(jī)搖晃的監(jiān)聽(tīng)器
本文給大家分享一段代碼實(shí)現(xiàn)檢測(cè)手機(jī)搖晃的監(jiān)聽(tīng)器,代碼簡(jiǎn)單易懂,非常不錯(cuò),感興趣的朋友參考下吧2016-12-12解決Android Studio日志太長(zhǎng)或滾動(dòng)太快問(wèn)題
這篇文章主要介紹了解決Android Studio日志太長(zhǎng)或滾動(dòng)太快問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-04-04Android開(kāi)發(fā)中Activity的生命周期及加載模式詳解
這篇文章主要介紹了Android開(kāi)發(fā)中Activity的生命周期及加載模式詳解的相關(guān)資料,非常不錯(cuò)具有參考借鑒價(jià)值,需要的朋友可以參考下2016-05-05Flutter開(kāi)發(fā)之Widget自定義總結(jié)
這篇文章主要給大家介紹了關(guān)于Flutter開(kāi)發(fā)中Widget自定義的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用Flutter具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04解決Android Studio導(dǎo)入項(xiàng)目非常慢的辦法
在使用Android studio的時(shí)候常常遇到這樣的問(wèn)題,從其他地方導(dǎo)入項(xiàng)目,Android studio呈現(xiàn)非常慢的現(xiàn)象!當(dāng)遇到這種情況時(shí),可以看看是下面這篇文章,再按照方法來(lái)解決!2016-09-09