Android10 啟動Zygote源碼解析
app_main
上一篇文章:
# Android 10 啟動分析之servicemanager篇 (二)
在init篇中有提到,init進程會在在Trigger 為late-init的Action中,啟動Zygote服務,這篇文章我們就來具體分析一下Zygote服務,去挖掘一下Zygote負責的工作。
Zygote服務的啟動入口源碼位于 /frameworks/base/cmds/app_process/app_main.cpp,我們將從這個文件的main方法開始解析。
int main(int argc, char* const argv[])
{
//聲明AppRuntime類的實例runtime,在AppRuntime類的構造方法中初始化的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位系統nice_name為zygote64; 32位系統為zygote
niceName = ZYGOTE_NICE_NAME;
} else if (strcmp(arg, "--start-system-server") == 0) {
//是否需要啟動system server
startSystemServer = true;
} else if (strcmp(arg, "--application") == 0) {
//啟動進入獨立的程序模式
application = true;
} else if (strncmp(arg, "--nice-name=", 12) == 0) {
//niceName 為當前進程別名,區(qū)別abi型號
niceName.setTo(arg + 12);
} else if (strncmp(arg, "--", 2) != 0) {
className.setTo(arg);
break;
} else {
--i;
break;
}
}
...
}
可以看到,app_main根據啟動時傳入參數的區(qū)別,分為zygote 模式和application模式。
我們可以從init.zygote64_32.rc文件中看到zygote的啟動參數為:
-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虛擬機Cache目錄和權限
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) {
//進入此分支
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.");
}
結合傳入的啟動參數來看,代碼將從if語句的else分支繼續(xù)往下執(zhí)行,進入zygote模式。至于application模式我們暫時先忽略它,等我們分析app的啟動過程時再來說明。
上述代碼最后將通過 runtime.start("com.android.internal.os.ZygoteInit", args, zygote);語句,將控制權限轉交給AppRuntime類去繼續(xù)執(zhí)行。
繼續(xù)從AppRuntime的start函數看起:
void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote)
{
...
// 虛擬機創(chuàng)建及啟動,主要是關于虛擬機參數的設置
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為系統響應的cpu架構類型,比如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"轉換為"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ù)找成員函數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調用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()函數主要做了三件事情,一調用startVm開啟虛擬機,二調用startReg注冊JNI方法,三就是使用JNI把Zygote進程啟動起來。
ZygoteInit
通過上述分析,代碼進入了ZygoteInit.java中的main方法繼續(xù)執(zhí)行。從這里開始,就真正的啟動了Zygote進程。
我們從/frameworks/base/core/java/com/android/internal/os/ZygoteInit.java這個文件繼續(xù)往下看。
public static void main(String argv[]) {
//ZygoteServer 是Zygote進程的Socket通訊服務端的管理類
ZygoteServer zygoteServer = null;
// 標記zygote啟動開始,調用ZygoteHooks的Jni方法,確保當前沒有其它線程在運行
ZygoteHooks.startZygoteNoThreadCreation();
//設置pid為0,Zygote進入自己的進程組
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()傳入的參數
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時,傳入了參數:start-system-server,會進入此分支
startSystemServer = true;
} else if ("--enable-lazy-preload".equals(argv[i])) {
//啟動zygote_secondary時,才會傳入參數: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]);
}
}
// 根據傳入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());
// 加載進程的資源和類
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();
// 調用ZygoteServer 構造函數,創(chuàng)建socket Server端,會根據傳入的參數,
// 創(chuàng)建兩個socket:/dev/socket/zygote 和 /dev/socket/zygote_secondary
zygoteServer = new ZygoteServer(isPrimaryZygote);
if (startSystemServer) {
//fork出system server進程
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進入無限循環(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方法中主要做了以下幾件事:
- 加載進程的資源和類。
- 根據傳入socket name來創(chuàng)建socket server。
- fork SystemServer 進程。
preload
既然preload方法是負責加載進程的資源和類,那么它究竟加載了哪些資源和哪些類呢,這些資源又位于什么位置呢?
我們先來看看preload方法里具體做了什么:
static void preload(TimingsTraceLog bootTimingsTraceLog) {
beginPreload();
//預加載類
preloadClasses();
cacheNonBootClasspathClassLoaders();
//加載圖片、顏色等資源文件
preloadResources();
//加載HAL相關內容
nativePreloadAppProcessHALs();
//加載圖形驅動
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文件的內容,并通過Class.forName初始化類。那么在/system/etc/preloaded-classes文件具體有哪些類呢?
由于內容過多,我這里只截取部分截圖讓大家看看具體裝載是什么類。



從裝載列表中,我們可以看到很多熟悉的類,實際上,裝載的類都是我們應用程序運行時可能用到的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里的內容很簡單,主要是加載位于/system/lib目錄下的libandroid.so、libcompiler_rt.so、libjnigraphics.so三個so庫。
我們不妨想一下,為什么android要在Zygote中將資源先進行預加載,這么做有什么好處?
這個問題留給各位讀者去自行思考,在這里便不再回答了。
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 {
//將啟動參數封裝到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子進程,最終會進入com_android_internal_os_Zygote.cpp 類中fork進程并做一些初始化操作
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需要主動關閉
zygoteServer.closeServerSocket();
// 裝載system server相關邏輯
return handleSystemServerProcess(parsedArgs);
}
return null;
}
forkSystemServer方法只是fork了一個Zygote的子進程,而handleSystemServerProcess方法構造了一個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,會進入此分支
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函數
//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代表應用程序退出時不調用AppRuntime.onExit(),否則會在退出前調用
nativeSetExitWithoutCleanup(true);
//設置虛擬機的內存利用率參數值為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用于構造一個Runnable。只要執(zhí)行此Runnable,就會開始調用com.android.server.SystemServer 類中的main方法。
到此,Zygote的邏輯已經全部執(zhí)行完畢,android啟動進入了SystemServer的階段。
最后,我們再用一個流程圖來總結一下Zygote的業(yè)務邏輯:

以上就是Android10 啟動Zygote源碼解析的詳細內容,更多關于Android10 啟動Zygote的資料請關注腳本之家其它相關文章!
相關文章
解決Android Studio 代碼自動提示突然失效的問題
這篇文章主要介紹了解決Android Studio 代碼自動提示突然失效的問題,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-03-03
Android中ListView的item點擊沒有反應的解決方法
這篇文章主要介紹了Android中ListView的item點擊沒有反應的相關資料,需要的朋友可以參考下2017-10-10
Android Service(不和用戶交互應用組件)案例分析
Service是在一段不定的時間運行在后臺,不和用戶交互應用組件,本文將詳細介紹,需要了解的朋友可以參考下2012-12-12
android開發(fā)教程之實現滑動關閉fragment示例
這篇文章主要介紹了android實現滑動關閉fragment示例,需要的朋友可以參考下2014-03-03
Android中的TimePickerView(時間選擇器)的用法詳解
這篇文章主要介紹了Android中的TimePickerView時間選擇器的用法,這是一個第三方從底部彈出來的日期選擇器,文中結合實例代碼給大家介紹的非常詳細,需要的朋友可以參考下2022-04-04

