<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
上一篇文章:
# 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 為當前程序別名,區別abi型號 niceName.setTo(arg + 12); } else if (strncmp(arg, "--", 2) != 0) { className.setTo(arg); break; } else { --i; break; } } ... }
可以看到,app_main根據啟動時傳入引數的區別,分為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分支繼續往下執行,進入zygote模式。至於application模式我們暫時先忽略它,等我們分析app的啟動過程時再來說明。
上述程式碼最後將通過 runtime.start("com.android.internal.os.ZygoteInit", args, zygote);
語句,將控制許可權轉交給AppRuntime類去繼續執行。
繼續從AppRuntime的start函數看起:
void AndroidRuntime::start(const char* className, const Vector<String8>& options, bool zygote) { ... // 虛擬機器器建立及啟動,主要是關於虛擬機器器引數的設定 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 nativesn"); 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 { //找到這個類後就繼續找成員函數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 VMn"); if (mJavaVM->DetachCurrentThread() != JNI_OK) ALOGW("Warning: unable to detach main threadn"); if (mJavaVM->DestroyJavaVM() != 0) ALOGW("Warning: VM did not shut down cleanlyn"); }
start()函數主要做了三件事情,一呼叫startVm開啟虛擬機器器,二呼叫startReg註冊JNI方法,三就是使用JNI把Zygote程序啟動起來。
通過上述分析,程式碼進入了ZygoteInit.java中的main方法繼續執行。從這裡開始,就真正的啟動了Zygote程序。
我們從/frameworks/base/core/java/com/android/internal/os/ZygoteInit.java
這個檔案繼續往下看。
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來決定是建立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,執行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 建構函式,建立socket Server端,會根據傳入的引數, // 建立兩個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進入無限迴圈,處理請求 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方法中主要做了以下幾件事:
既然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; }
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類。
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
中,其他的資源路徑也可以類似找到。各位讀者可以自行去該路徑下去看看所包含的資原始檔到底是什麼樣的。
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中將資源先進行預載入,這麼做有什麼好處?
這個問題留給各位讀者去自行思考,在這裡便不再回答了。
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物件,建立一個子執行緒用於啟動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 */ }
繼續從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); }
繼續看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。只要執行此Runnable,就會開始呼叫com.android.server.SystemServer
類中的main方法。
到此,Zygote的邏輯已經全部執行完畢,android啟動進入了SystemServer的階段。
最後,我們再用一個流程圖來總結一下Zygote的業務邏輯:
以上就是Android10 啟動Zygote原始碼解析的詳細內容,更多關於Android10 啟動Zygote的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45