Android10 啟動Zygote源碼解析
app_main
上一篇文章:
# Android 10 啟動分析之servicemanager篇 (二)
在init篇中有提到,init進(jìn)程會在在Trigger 為late-init的Action中,啟動Zygote服務(wù),這篇文章我們就來具體分析一下Zygote服務(wù),去挖掘一下Zygote負(fù)責(zé)的工作。
Zygote服務(wù)的啟動入口源碼位于 /frameworks/base/cmds/app_process/app_main.cpp
,我們將從這個文件的main方法開始解析。
int main(int argc, char* const argv[]) { //聲明AppRuntime類的實(shí)例runtime,在AppRuntime類的構(gòu)造方法中初始化的skia圖形引擎 AppRuntime runtime(argv[0], computeArgBlockSize(argc, argv)); ... bool zygote = false; bool startSystemServer = false; bool application = false; String8 niceName; String8 className; ++i; // Skip unused "parent dir" argument. while (i < argc) { const char* arg = argv[i++]; if (strcmp(arg, "--zygote") == 0) { zygote = true; //對于64位系統(tǒng)nice_name為zygote64; 32位系統(tǒng)為zygote niceName = ZYGOTE_NICE_NAME; } else if (strcmp(arg, "--start-system-server") == 0) { //是否需要啟動system server startSystemServer = true; } else if (strcmp(arg, "--application") == 0) { //啟動進(jìn)入獨(dú)立的程序模式 application = true; } else if (strncmp(arg, "--nice-name=", 12) == 0) { //niceName 為當(dāng)前進(jìn)程別名,區(qū)別abi型號 niceName.setTo(arg + 12); } else if (strncmp(arg, "--", 2) != 0) { className.setTo(arg); break; } else { --i; break; } } ... }
可以看到,app_main根據(jù)啟動時傳入?yún)?shù)的區(qū)別,分為zygote 模式和application模式。
我們可以從init.zygote64_32.rc
文件中看到zygote的啟動參數(shù)為:
-Xzygote /system/bin --zygote --start-system-server --socket-name=zygote
我們接著往下看:
Vector<String8> args; if (!className.isEmpty()) { // We're not in zygote mode, the only argument we need to pass // to RuntimeInit is the application argument. // // The Remainder of args get passed to startup class main(). Make // copies of them before we overwrite them with the process name. args.add(application ? String8("application") : String8("tool")); runtime.setClassNameAndArgs(className, argc - i, argv + i); if (!LOG_NDEBUG) { String8 restOfArgs; char* const* argv_new = argv + i; int argc_new = argc - i; for (int k = 0; k < argc_new; ++k) { restOfArgs.append("""); restOfArgs.append(argv_new[k]); restOfArgs.append("" "); } ALOGV("Class name = %s, args = %s", className.string(), restOfArgs.string()); } } else { // We're in zygote mode. //初始化Dalvik虛擬機(jī)Cache目錄和權(quán)限 maybeCreateDalvikCache(); if (startSystemServer) { //附加上start-system-serve 的arg args.add(String8("start-system-serve 的argr")); } char prop[PROP_VALUE_MAX]; if (property_get(ABI_LIST_PROPERTY, prop, NULL) == 0) { LOG_ALWAYS_FATAL("app_process: Unable to determine ABI list from property %s.", ABI_LIST_PROPERTY); return 11; } String8 abiFlag("--abi-list="); abiFlag.append(prop); args.add(abiFlag); // In zygote mode, pass all remaining arguments to the zygote // main() method. for (; i < argc; ++i) { args.add(String8(argv[i])); } } if (!niceName.isEmpty()) { runtime.setArgv0(niceName.string(), true /* setProcName */); } if (zygote) { //進(jìn)入此分支 runtime.start("com.android.internal.os.ZygoteInit", args, zygote); } else if (className) { runtime.start("com.android.internal.os.RuntimeInit", args, zygote); } else { fprintf(stderr, "Error: no class name or --zygote supplied.\n"); app_usage(); LOG_ALWAYS_FATAL("app_process: no class name or --zygote supplied."); }
結(jié)合傳入的啟動參數(shù)來看,代碼將從if語句的else分支繼續(xù)往下執(zhí)行,進(jìn)入zygote模式。至于application模式我們暫時先忽略它,等我們分析app的啟動過程時再來說明。
上述代碼最后將通過 runtime.start("com.android.internal.os.ZygoteInit", args, zygote);
語句,將控制權(quán)限轉(zhuǎn)交給AppRuntime類去繼續(xù)執(zhí)行。
繼續(xù)從AppRuntime的start函數(shù)看起:
void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote) { ... // 虛擬機(jī)創(chuàng)建及啟動,主要是關(guān)于虛擬機(jī)參數(shù)的設(shè)置 JniInvocation jni_invocation; jni_invocation.Init(NULL); JNIEnv* env; if (startVm(&mJavaVM, &env, zygote) != 0) { return; } onVmCreated(env); //注冊JNI方法 if (startReg(env) < 0) { ALOGE("Unable to register all android natives\n"); return; } /* * We want to call main() with a String array with arguments in it. * At present we have two arguments, the class name and an option string. * Create an array to hold them. */ jclass stringClass; jobjectArray strArray; jstring classNameStr; //等價于strArray[0] = "com.android.internal.os.ZygoteInit" stringClass = env->FindClass("java/lang/String"); assert(stringClass != NULL); strArray = env->NewObjectArray(options.size() + 1, stringClass, NULL); assert(strArray != NULL); classNameStr = env->NewStringUTF(className); assert(classNameStr != NULL); env->SetObjectArrayElement(strArray, 0, classNameStr); //strArray[1] = "start-system-server"; //strArray[2] = "--abi-list=xxx"; //其中xxx為系統(tǒng)響應(yīng)的cpu架構(gòu)類型,比如arm64-v8a. for (size_t i = 0; i < options.size(); ++i) { jstring optionsStr = env->NewStringUTF(options.itemAt(i).string()); assert(optionsStr != NULL); env->SetObjectArrayElement(strArray, i + 1, optionsStr); } /* * Start VM. This thread becomes the main thread of the VM, and will * not return until the VM exits. */ //將"com.android.internal.os.ZygoteInit"轉(zhuǎn)換為"com/android/internal/os/ZygoteInit char* slashClassName = toSlashClassName(className != NULL ? className : ""); jclass startClass = env->FindClass(slashClassName); if (startClass == NULL) { ALOGE("JavaVM unable to locate class '%s'\n", slashClassName); /* keep going */ } else { //找到這個類后就繼續(xù)找成員函數(shù)main方法的Mehtod ID jmethodID startMeth = env->GetStaticMethodID(startClass, "main", "([Ljava/lang/String;)V"); if (startMeth == NULL) { ALOGE("JavaVM unable to find main() in '%s'\n", className); /* keep going */ } else { // 通過Jni調(diào)用ZygoteInit.main()方法 env->CallStaticVoidMethod(startClass, startMeth, strArray); #if 0 if (env->ExceptionCheck()) threadExitUncaughtException(env); #endif } } free(slashClassName); ALOGD("Shutting down VM\n"); if (mJavaVM->DetachCurrentThread() != JNI_OK) ALOGW("Warning: unable to detach main thread\n"); if (mJavaVM->DestroyJavaVM() != 0) ALOGW("Warning: VM did not shut down cleanly\n"); }
start()函數(shù)主要做了三件事情,一調(diào)用startVm開啟虛擬機(jī),二調(diào)用startReg注冊JNI方法,三就是使用JNI把Zygote進(jìn)程啟動起來。
ZygoteInit
通過上述分析,代碼進(jìn)入了ZygoteInit.java中的main方法繼續(xù)執(zhí)行。從這里開始,就真正的啟動了Zygote進(jìn)程。
我們從/frameworks/base/core/java/com/android/internal/os/ZygoteInit.java
這個文件繼續(xù)往下看。
public static void main(String argv[]) { //ZygoteServer 是Zygote進(jìn)程的Socket通訊服務(wù)端的管理類 ZygoteServer zygoteServer = null; // 標(biāo)記zygote啟動開始,調(diào)用ZygoteHooks的Jni方法,確保當(dāng)前沒有其它線程在運(yùn)行 ZygoteHooks.startZygoteNoThreadCreation(); //設(shè)置pid為0,Zygote進(jìn)入自己的進(jìn)程組 try { Os.setpgid(0, 0); } catch (ErrnoException ex) { throw new RuntimeException("Failed to setpgid(0,0)", ex); } Runnable caller; try { ... //開啟DDMS(Dalvik Debug Monitor Service)功能 RuntimeInit.enableDdms(); //解析app_main.cpp - start()傳入的參數(shù) boolean startSystemServer = false; String zygoteSocketName = "zygote"; String abiList = null; boolean enableLazyPreload = false; for (int i = 1; i < argv.length; i++) { if ("start-system-server".equals(argv[i])) { //啟動zygote時,傳入了參數(shù):start-system-server,會進(jìn)入此分支 startSystemServer = true; } else if ("--enable-lazy-preload".equals(argv[i])) { //啟動zygote_secondary時,才會傳入?yún)?shù):enable-lazy-preload enableLazyPreload = true; } else if (argv[i].startsWith(ABI_LIST_ARG)) { abiList = argv[i].substring(ABI_LIST_ARG.length()); } else if (argv[i].startsWith(SOCKET_NAME_ARG)) { //SOCKET_NAME_ARG 為 zygote 或zygote_secondary,具體請參考 init.zyoget64_32.rc文件 zygoteSocketName = argv[i].substring(SOCKET_NAME_ARG.length()); } else { throw new RuntimeException("Unknown command line argument: " + argv[i]); } } // 根據(jù)傳入socket name來決定是創(chuàng)建socket還是zygote_secondary final boolean isPrimaryZygote = zygoteSocketName.equals(Zygote.PRIMARY_SOCKET_NAME); if (abiList == null) { throw new RuntimeException("No ABI list supplied."); } // In some configurations, we avoid preloading resources and classes eagerly. // In such cases, we will preload things prior to our first fork. // 在第一次zygote啟動時,enableLazyPreload為false,執(zhí)行preload if (!enableLazyPreload) { bootTimingsTraceLog.traceBegin("ZygotePreload"); EventLog.writeEvent(LOG_BOOT_PROGRESS_PRELOAD_START, SystemClock.uptimeMillis()); // 加載進(jìn)程的資源和類 preload(bootTimingsTraceLog); EventLog.writeEvent(LOG_BOOT_PROGRESS_PRELOAD_END, SystemClock.uptimeMillis()); bootTimingsTraceLog.traceEnd(); // ZygotePreload } else { Zygote.resetNicePriority(); } // Do an initial gc to clean up after startup bootTimingsTraceLog.traceBegin("PostZygoteInitGC"); gcAndFinalize(); bootTimingsTraceLog.traceEnd(); // PostZygoteInitGC bootTimingsTraceLog.traceEnd(); // ZygoteInit // Disable tracing so that forked processes do not inherit stale tracing tags from // Zygote. Trace.setTracingEnabled(false, 0); Zygote.initNativeState(isPrimaryZygote); ZygoteHooks.stopZygoteNoThreadCreation(); // 調(diào)用ZygoteServer 構(gòu)造函數(shù),創(chuàng)建socket Server端,會根據(jù)傳入的參數(shù), // 創(chuàng)建兩個socket:/dev/socket/zygote 和 /dev/socket/zygote_secondary zygoteServer = new ZygoteServer(isPrimaryZygote); if (startSystemServer) { //fork出system server進(jìn)程 Runnable r = forkSystemServer(abiList, zygoteSocketName, zygoteServer); // {@code r == null} in the parent (zygote) process, and {@code r != null} in the // child (system_server) process. if (r != null) { // 啟動SystemServer r.run(); return; } } Log.i(TAG, "Accepting command socket connections"); // ZygoteServer進(jìn)入無限循環(huán),處理請求 caller = zygoteServer.runSelectLoop(abiList); } catch (Throwable ex) { Log.e(TAG, "System zygote died with exception", ex); throw ex; } finally { if (zygoteServer != null) {4 zygoteServer.closeServerSocket(); } } // We're in the child process and have exited the select loop. Proceed to execute the // command. if (caller != null) { caller.run(); } }
main方法中主要做了以下幾件事:
- 加載進(jìn)程的資源和類。
- 根據(jù)傳入socket name來創(chuàng)建socket server。
- fork SystemServer 進(jìn)程。
preload
既然preload方法是負(fù)責(zé)加載進(jìn)程的資源和類,那么它究竟加載了哪些資源和哪些類呢,這些資源又位于什么位置呢?
我們先來看看preload方法里具體做了什么:
static void preload(TimingsTraceLog bootTimingsTraceLog) { beginPreload(); //預(yù)加載類 preloadClasses(); cacheNonBootClasspathClassLoaders(); //加載圖片、顏色等資源文件 preloadResources(); //加載HAL相關(guān)內(nèi)容 nativePreloadAppProcessHALs(); //加載圖形驅(qū)動 maybePreloadGraphicsDriver(); // 加載 android、compiler_rt、jnigraphics等library preloadSharedLibraries(); //用于初始化文字資源 preloadTextResources(); //用于初始化webview; WebViewFactory.prepareWebViewInZygote(); endPreload(); warmUpJcaProviders(); sPreloadComplete = true; }
preloadClasses
private static void preloadClasses() { final VMRuntime runtime = VMRuntime.getRuntime(); //preload classes 路徑為 /system/etc/preloaded-classes InputStream is; try { is = new FileInputStream(PRELOADED_CLASSES); } catch (FileNotFoundException e) { Log.e(TAG, "Couldn't find " + PRELOADED_CLASSES + "."); return; } ... try { BufferedReader br = new BufferedReader(new InputStreamReader(is), Zygote.SOCKET_BUFFER_SIZE); int count = 0; String line; while ((line = br.readLine()) != null) { // Skip comments and blank lines. line = line.trim(); if (line.startsWith("#") || line.equals("")) { continue; } Trace.traceBegin(Trace.TRACE_TAG_DALVIK, line); try { //使用Class.forName初始化類 Class.forName(line, true, null); count++; } catch (ClassNotFoundException e) { Log.w(TAG, "Class not found for preloading: " + line); } catch (UnsatisfiedLinkError e) { Log.w(TAG, "Problem preloading " + line + ": " + e); } catch (Throwable t) { Log.e(TAG, "Error preloading " + line + ".", t); if (t instanceof Error) { throw (Error) t; } if (t instanceof RuntimeException) { throw (RuntimeException) t; } throw new RuntimeException(t); } Trace.traceEnd(Trace.TRACE_TAG_DALVIK); } } catch (IOException e) { Log.e(TAG, "Error reading " + PRELOADED_CLASSES + ".", e); } finally { ... } }
可以看到,preloadClasses方法讀取/system/etc/preloaded-classes文件的內(nèi)容,并通過Class.forName初始化類。那么在/system/etc/preloaded-classes文件具體有哪些類呢?
由于內(nèi)容過多,我這里只截取部分截圖讓大家看看具體裝載是什么類。
從裝載列表中,我們可以看到很多熟悉的類,實(shí)際上,裝載的類都是我們應(yīng)用程序運(yùn)行時可能用到的java類。
preloadResources
private static void preloadResources() { final VMRuntime runtime = VMRuntime.getRuntime(); try { mResources = Resources.getSystem(); mResources.startPreloading(); if (PRELOAD_RESOURCES) { Log.i(TAG, "Preloading resources..."); long startTime = SystemClock.uptimeMillis(); //裝載com.android.internal.R.array.preloaded_drawables中的圖片資源 TypedArray ar = mResources.obtainTypedArray( com.android.internal.R.array.preloaded_drawables); int N = preloadDrawables(ar); ar.recycle(); Log.i(TAG, "...preloaded " + N + " resources in " + (SystemClock.uptimeMillis() - startTime) + "ms."); startTime = SystemClock.uptimeMillis(); //裝載com.android.internal.R.array.preloaded_color_state_lists中的顏色資源 ar = mResources.obtainTypedArray( com.android.internal.R.array.preloaded_color_state_lists); N = preloadColorStateLists(ar); ar.recycle(); Log.i(TAG, "...preloaded " + N + " resources in " + (SystemClock.uptimeMillis() - startTime) + "ms."); if (mResources.getBoolean( com.android.internal.R.bool.config_freeformWindowManagement)) { startTime = SystemClock.uptimeMillis(); //裝載com.android.internal.R.array.preloaded_freeform_multi_window_drawables中的圖片資源 ar = mResources.obtainTypedArray( com.android.internal.R.array.preloaded_freeform_multi_window_drawables); N = preloadDrawables(ar); ar.recycle(); Log.i(TAG, "...preloaded " + N + " resource in " + (SystemClock.uptimeMillis() - startTime) + "ms."); } } mResources.finishPreloading(); } catch (RuntimeException e) { Log.w(TAG, "Failure preloading resources", e); } }
從上述代碼可以看到,preloadResources加載了特定的圖片資源和顏色資源。這些資源的路徑又具體在哪里呢?
com.android.internal.R.array.preloaded_drawables
的路徑位于/frameworks/base/core/res/res/values/arrays.xml
中,其他的資源路徑也可以類似找到。各位讀者可以自行去該路徑下去看看所包含的資源文件到底是什么樣的。
preloadSharedLibraries
private static void preloadSharedLibraries() { Log.i(TAG, "Preloading shared libraries..."); System.loadLibrary("android"); System.loadLibrary("compiler_rt"); System.loadLibrary("jnigraphics"); }
preloadSharedLibraries
里的內(nèi)容很簡單,主要是加載位于/system/lib目錄下的libandroid.so、libcompiler_rt.so、libjnigraphics.so三個so庫。
我們不妨想一下,為什么android要在Zygote中將資源先進(jìn)行預(yù)加載,這么做有什么好處?
這個問題留給各位讀者去自行思考,在這里便不再回答了。
forkSystemServer
private static Runnable forkSystemServer(String abiList, String socketName, ZygoteServer zygoteServer) { ... //配置system server String args[] = { "--setuid=1000", "--setgid=1000", "--setgroups=1001,1002,1003,1004,1005,1006,1007,1008,1009,1010,1018,1021,1023," + "1024,1032,1065,3001,3002,3003,3006,3007,3009,3010", "--capabilities=" + capabilities + "," + capabilities, "--nice-name=system_server", "--runtime-args", "--target-sdk-version=" + VMRuntime.SDK_VERSION_CUR_DEVELOPMENT, "com.android.server.SystemServer", }; ZygoteArguments parsedArgs = null; int pid; try { //將啟動參數(shù)封裝到ZygoteArguments類中 parsedArgs = new ZygoteArguments(args); Zygote.applyDebuggerSystemProperty(parsedArgs); Zygote.applyInvokeWithSystemProperty(parsedArgs); boolean profileSystemServer = SystemProperties.getBoolean( "dalvik.vm.profilesystemserver", false); if (profileSystemServer) { parsedArgs.mRuntimeFlags |= Zygote.PROFILE_SYSTEM_SERVER; } //fork systemserver子進(jìn)程,最終會進(jìn)入com_android_internal_os_Zygote.cpp 類中fork進(jìn)程并做一些初始化操作 pid = Zygote.forkSystemServer( parsedArgs.mUid, parsedArgs.mGid, parsedArgs.mGids, parsedArgs.mRuntimeFlags, null, parsedArgs.mPermittedCapabilities, parsedArgs.mEffectiveCapabilities); } catch (IllegalArgumentException ex) { throw new RuntimeException(ex); } if (pid == 0) { //pid == 0 ,處理system server的邏輯 if (hasSecondZygote(abiList)) { // 處理32_64和64_32的情況 waitForSecondaryZygote(socketName); } // fork時會copy socket,system server需要主動關(guān)閉 zygoteServer.closeServerSocket(); // 裝載system server相關(guān)邏輯 return handleSystemServerProcess(parsedArgs); } return null; }
forkSystemServer
方法只是fork了一個Zygote的子進(jìn)程,而handleSystemServerProcess
方法構(gòu)造了一個Runnable對象,創(chuàng)建一個子線程用于啟動SystemServer的邏輯。
private static Runnable handleSystemServerProcess(ZygoteArguments parsedArgs) { Os.umask(S_IRWXG | S_IRWXO); if (parsedArgs.mNiceName != null) { //nicename 為 system_server Process.setArgV0(parsedArgs.mNiceName); } ... if (parsedArgs.mInvokeWith != null) { String[] args = parsedArgs.mRemainingArgs; // If we have a non-null system server class path, we'll have to duplicate the // existing arguments and append the classpath to it. ART will handle the classpath // correctly when we exec a new process. if (systemServerClasspath != null) { String[] amendedArgs = new String[args.length + 2]; amendedArgs[0] = "-cp"; amendedArgs[1] = systemServerClasspath; System.arraycopy(args, 0, amendedArgs, 2, args.length); args = amendedArgs; } WrapperInit.execApplication(parsedArgs.mInvokeWith, parsedArgs.mNiceName, parsedArgs.mTargetSdkVersion, VMRuntime.getCurrentInstructionSet(), null, args); throw new IllegalStateException("Unexpected return from WrapperInit.execApplication"); } else { //parsedArgs.mInvokeWith 為null,會進(jìn)入此分支 createSystemServerClassLoader(); ClassLoader cl = sCachedSystemServerClassLoader; if (cl != null) { Thread.currentThread().setContextClassLoader(cl); } /* * Pass the remaining arguments to SystemServer. */ return ZygoteInit.zygoteInit(parsedArgs.mTargetSdkVersion, parsedArgs.mRemainingArgs, cl); } /* should never reach here */ }
繼續(xù)從ZygoteInit.zygoteInit
看起:
public static final Runnable zygoteInit(int targetSdkVersion, String[] argv, ClassLoader classLoader) { ... RuntimeInit.commonInit(); //注冊兩個jni函數(shù) //android_internal_os_ZygoteInit_nativePreloadAppProcessHALs //android_internal_os_ZygoteInit_nativePreloadGraphicsDriver ZygoteInit.nativeZygoteInit(); return RuntimeInit.applicationInit(targetSdkVersion, argv, classLoader); }
RuntimeInit.applicationInit
:
protected static Runnable applicationInit(int targetSdkVersion, String[] argv, ClassLoader classLoader) { //true代表應(yīng)用程序退出時不調(diào)用AppRuntime.onExit(),否則會在退出前調(diào)用 nativeSetExitWithoutCleanup(true); //設(shè)置虛擬機(jī)的內(nèi)存利用率參數(shù)值為0.75 VMRuntime.getRuntime().setTargetHeapUtilization(0.75f); VMRuntime.getRuntime().setTargetSdkVersion(targetSdkVersion); final Arguments args = new Arguments(argv); // Remaining arguments are passed to the start class's static main return findStaticMain(args.startClass, args.startArgs, classLoader); }
繼續(xù)看findStaticMain
:
protected static Runnable findStaticMain(String className, String[] argv, ClassLoader classLoader) { Class<?> cl; try { //這里className為 com.android.server.SystemServer cl = Class.forName(className, true, classLoader); } catch (ClassNotFoundException ex) { throw new RuntimeException( "Missing class when invoking static main " + className, ex); } Method m; try { m = cl.getMethod("main", new Class[] { String[].class }); } catch (NoSuchMethodException ex) { throw new RuntimeException( "Missing static main on " + className, ex); } catch (SecurityException ex) { throw new RuntimeException( "Problem getting static main on " + className, ex); } int modifiers = m.getModifiers(); if (! (Modifier.isStatic(modifiers) && Modifier.isPublic(modifiers))) { throw new RuntimeException( "Main method is not public and static on " + className); } /* * This throw gets caught in ZygoteInit.main(), which responds * by invoking the exception's run() method. This arrangement * clears up all the stack frames that were required in setting * up the process. */ return new MethodAndArgsCaller(m, argv); }
這里通過反射獲得了 com.android.server.SystemServer
類中的main方法,并傳遞給MethodAndArgsCaller用于構(gòu)造一個Runnable。只要執(zhí)行此Runnable,就會開始調(diào)用com.android.server.SystemServer
類中的main方法。
到此,Zygote的邏輯已經(jīng)全部執(zhí)行完畢,android啟動進(jìn)入了SystemServer的階段。
最后,我們再用一個流程圖來總結(jié)一下Zygote的業(yè)務(wù)邏輯:
以上就是Android10 啟動Zygote源碼解析的詳細(xì)內(nèi)容,更多關(guān)于Android10 啟動Zygote的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Jenkins打包android應(yīng)用時自動簽名apk詳解
這篇文章主要介紹了Jenkins打包android應(yīng)用時自動簽名apk詳解,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2019-07-07Android Http實(shí)現(xiàn)文件的上傳和下載
這篇文章主要為大家詳細(xì)介紹了Android Http實(shí)現(xiàn)文件的上傳和下載,感興趣的小伙伴們可以參考一下2016-08-08解決Android Studio 代碼自動提示突然失效的問題
這篇文章主要介紹了解決Android Studio 代碼自動提示突然失效的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-03-03Android7.0版本影響開發(fā)的改進(jìn)分析
這篇文章主要介紹了Android7.0版本影響開發(fā)的改進(jìn),總結(jié)分析了Android7.0版本中比較常見的開發(fā)注意事項(xiàng)與操作技巧,需要的朋友可以參考下2017-11-11Android實(shí)現(xiàn)拍照、選擇相冊圖片并裁剪功能
這篇文章主要為大家詳細(xì)介紹了Android實(shí)現(xiàn)拍照、選擇相冊圖片并裁剪功能的相關(guān)資料,具有一定的參考價值,感興趣的小伙伴們可以參考一下2016-12-12Android中ListView的item點(diǎn)擊沒有反應(yīng)的解決方法
這篇文章主要介紹了Android中ListView的item點(diǎn)擊沒有反應(yīng)的相關(guān)資料,需要的朋友可以參考下2017-10-10Android Service(不和用戶交互應(yīng)用組件)案例分析
Service是在一段不定的時間運(yùn)行在后臺,不和用戶交互應(yīng)用組件,本文將詳細(xì)介紹,需要了解的朋友可以參考下2012-12-12android開發(fā)教程之實(shí)現(xiàn)滑動關(guān)閉fragment示例
這篇文章主要介紹了android實(shí)現(xiàn)滑動關(guān)閉fragment示例,需要的朋友可以參考下2014-03-03Android中的TimePickerView(時間選擇器)的用法詳解
這篇文章主要介紹了Android中的TimePickerView時間選擇器的用法,這是一個第三方從底部彈出來的日期選擇器,文中結(jié)合實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2022-04-04