IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 移动开发 -> 安卓源码分析-从请求创建应用程序进程到ActivityThread.Main方法 -> 正文阅读

[移动开发]安卓源码分析-从请求创建应用程序进程到ActivityThread.Main方法

安卓源码分析-应用程序进程启动过程

本文基于刘望舒大佬著作《Android进阶解密》结合最新源码整理,丰富而成。内部包含我个人的理解,可能有误

本文基于安卓源码版本9.0.0_r3

Ams发送启动应用程序进程请求

由ActivityManagerService发送启动应用程序进程请求,
在ActivityManagerService类startProcess方法中调用了Process.start,其中传入的uid,和pid在后面有用到

/frameworks/base/services/core/java/com/android/server/am/ActivityManagerService.java
4463    private ProcessStartResult startProcess(String hostingType, String entryPoint,
4464            ProcessRecord app, int uid, int[] gids, int runtimeFlags, int mountExternal,
4465            String seInfo, String requiredAbi, String instructionSet, String invokeWith,
4466            long startTime) {
4467        try {
4468            Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "Start proc: " +
4469                    app.processName);
4470            checkTime(startTime, "startProcess: asking zygote to start proc");
4471            final ProcessStartResult startResult;
4472            if (hostingType.equals("webview_service")) {
4473                startResult = startWebView(entryPoint,
4474                        app.processName, uid, uid, gids, runtimeFlags, mountExternal,
4475                        app.info.targetSdkVersion, seInfo, requiredAbi, instructionSet,
4476                        app.info.dataDir, null,
4477                        new String[] {PROC_START_SEQ_IDENT + app.startSeq});
4478            } else {
4479                startResult = Process.start(entryPoint,
4480                        app.processName, uid, uid, gids, runtimeFlags, mountExternal,
4481                        app.info.targetSdkVersion, seInfo, requiredAbi, instructionSet,
4482                        app.info.dataDir, invokeWith,
4483                        new String[] {PROC_START_SEQ_IDENT + app.startSeq});
4484            }
4485            checkTime(startTime, "startProcess: returned from zygote!");
4486            return startResult;
4487        } finally {
4488            Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
4489        }
4490    }

Process.start()内部直接返回zygoteProcess.start()
zygoteProcess.start()内部直接返回
startViaZygote
()方法

/frameworks/base/core/java/android/os/ZygoteProcess.java
357  private Process.ProcessStartResult startViaZygote(final String processClass,
358                                                      final String niceName,
359                                                      final int uid, final int gid,
360                                                      final int[] gids,
361                                                      int runtimeFlags, int mountExternal,
362                                                      int targetSdkVersion,
363                                                      String seInfo,
364                                                      String abi,
365                                                      String instructionSet,
366                                                      String appDataDir,
367                                                      String invokeWith,
368                                                      boolean startChildZygote,
369                                                      String[] extraArgs)
370                                                      throws ZygoteStartFailedEx {
371        ArrayList<String> argsForZygote = new ArrayList<String>();
372
373        // --runtime-args, --setuid=, --setgid=,
374        // and --setgroups= must go first
375        argsForZygote.add("--runtime-args");
376        argsForZygote.add("--setuid=" + uid);
377        argsForZygote.add("--setgid=" + gid);
378        argsForZygote.add("--runtime-flags=" + runtimeFlags);
379        if (mountExternal == Zygote.MOUNT_EXTERNAL_DEFAULT) {
380            argsForZygote.add("--mount-external-default");
381        } else if (mountExternal == Zygote.MOUNT_EXTERNAL_READ) {
382            argsForZygote.add("--mount-external-read");
383        } else if (mountExternal == Zygote.MOUNT_EXTERNAL_WRITE) {
384            argsForZygote.add("--mount-external-write");
385        }
386        argsForZygote.add("--target-sdk-version=" + targetSdkVersion);
387
......
437 synchronized(mLock) {
438            return zygoteSendArgsAndGetResult(openZygoteSocketIfNeeded(abi), argsForZygote);
439        }

之前传入的UID,PID在这个作为参数添加进argsForZygote 这个list集合中。
方法最后返回zygoteSendArgsAndGetResult方法,
传入值为
openZygoteSocketIfNeeded方法返回的zygotestate和argsForZygote这个list。

/frameworks/base/core/java/android/os/ZygoteProcess.java
280  private static Process.ProcessStartResult zygoteSendArgsAndGetResult(
281            ZygoteState zygoteState, ArrayList<String> args)
282            throws ZygoteStartFailedEx {
283        try {
284            // Throw early if any of the arguments are malformed. This means we can
285            // avoid writing a partial response to the zygote.
286            int sz = args.size();
287            for (int i = 0; i < sz; i++) {
288                if (args.get(i).indexOf('\n') >= 0) {
289                    throw new ZygoteStartFailedEx("embedded newlines not allowed");
290                }
291            }
292
293            /**
294             * See com.android.internal.os.SystemZygoteInit.readArgumentList()
295             * Presently the wire format to the zygote process is:
296             * a) a count of arguments (argc, in essence)
297             * b) a number of newline-separated argument strings equal to count
298             *
299             * After the zygote process reads these it will write the pid of
300             * the child or -1 on failure, followed by boolean to
301             * indicate whether a wrapper process was used.
302             */
303            final BufferedWriter writer = zygoteState.writer;
304            final DataInputStream inputStream = zygoteState.inputStream;
305
306            writer.write(Integer.toString(args.size()));
307            writer.newLine();
308
309            for (int i = 0; i < sz; i++) {
310                String arg = args.get(i);
311                writer.write(arg);
312                writer.newLine();
313            }
314
315            writer.flush();
316
317            // Should there be a timeout on this?
318            Process.ProcessStartResult result = new Process.ProcessStartResult();
319
320            // Always read the entire result from the input stream to avoid leaving
321            // bytes in the stream for future process starts to accidentally stumble
322            // upon.
323            result.pid = inputStream.readInt();
324            result.usingWrapper = inputStream.readBoolean();
325
326            if (result.pid < 0) {
327                throw new ZygoteStartFailedEx("fork() failed");
328            }
329            return result;
330        } catch (IOException ex) {
331            zygoteState.close();
332            throw new ZygoteStartFailedEx(ex);
333        }
334    }

可以看到,zygoteSendArgsAndGetResult把传入的写了启动参数的list写入同样作为传参在之前调用openZygoteSocketIfNeeded方法返回的ZygoteState中,zygotestate是zygoteprocess静态内部类。表示与zygote通讯状态。
回到上面,我们来看看openZygoteSocketIfNeeded的具体执行

563    private ZygoteState openZygoteSocketIfNeeded(String abi) throws ZygoteStartFailedEx {
564        Preconditions.checkState(Thread.holdsLock(mLock), "ZygoteProcess lock not held");
565
566        if (primaryZygoteState == null || primaryZygoteState.isClosed()) {
567            try {
				   //与zygote建立socket连接
568                primaryZygoteState = ZygoteState.connect(mSocket);
569            } catch (IOException ioe) {
570                throw new ZygoteStartFailedEx("Error connecting to primary zygote", ioe);
571            }
572            maybeSetApiBlacklistExemptions(primaryZygoteState, false);
573            maybeSetHiddenApiAccessLogSampleRate(primaryZygoteState);
574        }
			//连接zygote主模式返回的zygoteState是否与应用程序进程所需要的ABI匹配
575        if (primaryZygoteState.matches(abi)) {
576            return primaryZygoteState;
577        }
578
579        // 如果不匹配,则尝试连接zygote辅模式
580        if (secondaryZygoteState == null || secondaryZygoteState.isClosed()) {
581            try {
582                secondaryZygoteState = ZygoteState.connect(mSecondarySocket);
583            } catch (IOException ioe) {
584                throw new ZygoteStartFailedEx("Error connecting to secondary zygote", ioe);
585            }
586            maybeSetApiBlacklistExemptions(secondaryZygoteState, false);
587            maybeSetHiddenApiAccessLogSampleRate(secondaryZygoteState);
588        }
589			//辅模式返回的zygoteState是否与应用程序abi匹配
590        if (secondaryZygoteState.matches(abi)) {
591            return secondaryZygoteState;
592        }
593
594        throw new ZygoteStartFailedEx("Unsupported zygote ABI: " + abi);
595    }

Android目前支持以下七种ABI:armeabi、armeabi-v7a、arm64-v8a、x86、x86_64、mips、mips64。主流的ABI包括:
armeabiv-v7a: 第7代及以上的 ARM 处理器。
arm64-v8a: 第8代、64位ARM处理器。
armeabi: 第5代、第6代的ARM处理器,早期的手机用的比较多。
x86: 平板、模拟器用得比较多。
x86_64: 64位的平板。
mips和mips64很少用,以下不予描述。

zygote进程的main方法会创建名为“zygote”的server端Socket,注释第一处进行连接,第二处判断连接的cpu架构是否和应用程序进程所需的架构匹配,如果不匹配就连接zygote的辅模式。如果辅模式还不匹配。那就抛异常了

Zygote接收请求并创建应用程序进程

zygote连接成功后不管主辅模式都会返回一个zygotestate。在前面的方法zygoteSendArgsAndGetResult里,会把存在list的启动参数写入zygotestate中。这样zygote就收到了创建进程的请求。前面说过,zygote进程的main方法会创建socket等待ams请求。下面看zygote进程main方法。

/frameworks/base/core/java/com/android/internal/os/ZygoteInit.java
779 	String socketName = "zygote";
780            String abiList = null;
781            boolean enableLazyPreload = false;
782            for (int i = 1; i < argv.length; i++) {
783                if ("start-system-server".equals(argv[i])) {
784                    startSystemServer = true;
785                } else if ("--enable-lazy-preload".equals(argv[i])) {
786                    enableLazyPreload = true;
787                } else if (argv[i].startsWith(ABI_LIST_ARG)) {
788                    abiList = argv[i].substring(ABI_LIST_ARG.length());
789                } else if (argv[i].startsWith(SOCKET_NAME_ARG)) {
790                    socketName = argv[i].substring(SOCKET_NAME_ARG.length());
791                } else {
792                    throw new RuntimeException("Unknown command line argument: " + argv[i]);
793                }
794            }
795
796            if (abiList == null) {
797                throw new RuntimeException("No ABI list supplied.");
798            }
				//创建server端的socket,socketname 为 zygote
800            zygoteServer.registerServerSocketFromEnv(socketName);
801            // In some configurations, we avoid preloading resources and classes eagerly.
802            // In such cases, we will preload things prior to our first fork.
803            if (!enableLazyPreload) {
804                bootTimingsTraceLog.traceBegin("ZygotePreload");
805                EventLog.writeEvent(LOG_BOOT_PROGRESS_PRELOAD_START,
806                    SystemClock.uptimeMillis());
					//预加载类和资源
807                preload(bootTimingsTraceLog);
808                EventLog.writeEvent(LOG_BOOT_PROGRESS_PRELOAD_END,
809                    SystemClock.uptimeMillis());
810                bootTimingsTraceLog.traceEnd(); // ZygotePreload
811            } else {
812                Zygote.resetNicePriority();
813            }
814
815            // Do an initial gc to clean up after startup
816            bootTimingsTraceLog.traceBegin("PostZygoteInitGC");
817            gcAndFinalize();
818            bootTimingsTraceLog.traceEnd(); // PostZygoteInitGC
819
820            bootTimingsTraceLog.traceEnd(); // ZygoteInit
821            // Disable tracing so that forked processes do not inherit stale tracing tags from
822            // Zygote.
823            Trace.setTracingEnabled(false, 0);
824
825            Zygote.nativeSecurityInit();
826
827            // Zygote process unmounts root storage spaces.
828            Zygote.nativeUnmountStorageOnInit();
829
830            ZygoteHooks.stopZygoteNoThreadCreation();
831
832            if (startSystemServer) {
					//创建systemserver启动的runable
833                Runnable r = forkSystemServer(abiList, socketName, zygoteServer);
834
835                // {@code r == null} in the parent (zygote) process, and {@code r != null} in the
836                // child (system_server) process.
837                if (r != null) {
						//运行线程
838                    r.run();
839                    return;
840                }
841            }
842
843            Log.i(TAG, "Accepting command socket connections");
844
845            // The select loop returns early in the child process after a fork and
846            // loops forever in the zygote.
				//开启循环,等待ams请求
847            caller = zygoteServer.runSelectLoop(abiList);
848        } catch (Throwable ex) {
849            Log.e(TAG, "System zygote died with exception", ex);
850            throw ex;
851        } finally {
852            zygoteServer.closeServerSocket();
853        }
854
855        // We're in the child process and have exited the select loop. Proceed to execute the
856        // command.
857        if (caller != null) {
858            caller.run();
859        }
860    }

zygote的main方法里,后面调用了zygoteServer.runSelectLoop方法
runSelectLoop方法经过层层调用,最后会返回一个Runnable对象,858行调用了其run方法。就是调用了ActivityThread类的main方法,在后面我会详细解释调用过程

   /frameworks/base/core/java/com/android/internal/os/ZygoteServer.java
173    Runnable runSelectLoop(String abiList) {
174        ArrayList<FileDescriptor> fds = new ArrayList<FileDescriptor>();
175        ArrayList<ZygoteConnection> peers = new ArrayList<ZygoteConnection>();
176
177        fds.add(mServerSocket.getFileDescriptor());
178        peers.add(null);
179
180        while (true) {
181            StructPollfd[] pollFds = new StructPollfd[fds.size()];
182            for (int i = 0; i < pollFds.length; ++i) {
183                pollFds[i] = new StructPollfd();
184                pollFds[i].fd = fds.get(i);
185                pollFds[i].events = (short) POLLIN;
186            }
187            try {
188                Os.poll(pollFds, -1);
189            } catch (ErrnoException ex) {
190                throw new RuntimeException("poll failed", ex);
191            }
192            for (int i = pollFds.length - 1; i >= 0; --i) {
193                if ((pollFds[i].revents & POLLIN) == 0) {
194                    continue;
195                }
196
197                if (i == 0) {
198                    ZygoteConnection newPeer = acceptCommandPeer(abiList);
199                    peers.add(newPeer);
200                    fds.add(newPeer.getFileDesciptor());
201                } else {
202    					try {
203                        ZygoteConnection connection = peers.get(i);
204                        final Runnable command = connection.processOneCommand(this);
205
206                        if (mIsForkChild) {
207                            
208                            // stage if processOneCommand hasn't called "exec".
209                            if (command == null) {
210                                throw new IllegalStateException("command == null");
211                            }
212
213                            return command;
214                        } else {
215                            // We're in the server - we should never have any commands to run.
216                            if (command != null) {
.......

zygote等到请求后,会调用204行处processOneCommand方法来创建应用程序进程。processOneCommand是ZygoteConnection类的方法

   /frameworks/base/core/java/com/android/internal/os/ZygoteConnection.java
123    Runnable processOneCommand(ZygoteServer zygoteServer) {
124        String args[];
125        Arguments parsedArgs = null;
126        FileDescriptor[] descriptors;
127
128        try {
			//调用readArgumentList方法从socket读取应用程序启动参数
129            args = readArgumentList();
130            descriptors = mSocket.getAncillaryFileDescriptors();
131        } catch (IOException ex) {
132            throw new IllegalStateException("IOException on command socket", ex);
133        }
134
135        // readArgumentList returns null only when it has reached EOF with no available
136        // data to read. This will only happen when the remote socket has disconnected.
137        if (args == null) {
138            isEof = true;
139            return null;
140        }
141
142        int pid = -1;
143        FileDescriptor childPipeFd = null;
144        FileDescriptor serverPipeFd = null;
145			//把应用程序启动参数封装
146        parsedArgs = new Arguments(args);
147
......

233			//创建应用程序进程
234        pid = Zygote.forkAndSpecialize(parsedArgs.uid, parsedArgs.gid, parsedArgs.gids,
235                parsedArgs.runtimeFlags, rlimits, parsedArgs.mountExternal, parsedArgs.seInfo,
236                parsedArgs.niceName, fdsToClose, fdsToIgnore, parsedArgs.startChildZygote,
237                parsedArgs.instructionSet, parsedArgs.appDataDir);
238
239        try {
240            if (pid == 0) {
241                // fork的函数在新创建的线程子线程
242                zygoteServer.setForkChild();
243
244                zygoteServer.closeServerSocket();
245                IoUtils.closeQuietly(serverPipeFd);
246                serverPipeFd = null;
247
248                return handleChildProc(parsedArgs, descriptors, childPipeFd,
249                        parsedArgs.startChildZygote);
250            } else {
251                // 
252                // handleParentProc.
253                IoUtils.closeQuietly(childPipeFd);
254                childPipeFd = null;
255                handleParentProc(pid, descriptors, serverPipeFd);
256                return null;
257            }
258        } finally {
259            IoUtils.closeQuietly(childPipeFd);
260            IoUtils.closeQuietly(serverPipeFd);
261        }

通过forkAndSpecialize方法来fork一个线程,看fork介绍

fork是UNIX或类UNIX中的分叉函数,fork函数将运行着的程序分成2个(几乎)完全一样的进程,每个进程都启动一个从代码的同一位置开始执行的线程。这两个进程中的线程继续执行,就像是两个用户同时启动了该应用程序的两个副本。

所以在后面判断pid是否等于0,如果等于0说明已经是运行在fork出的新线程里面了。
然后返回handleChildProc执行应用程序进程,handleChildProc方法内部重要的先判断当前进程是不是zygote进程

/frameworks/base/core/java/com/android/internal/os/ZygoteConnection.java
882 if (!isZygote) {
883                return ZygoteInit.zygoteInit(parsedArgs.targetSdkVersion, parsedArgs.remainingArgs,
884                        null /* classLoader */);
885            } else {
886                return ZygoteInit.childZygoteInit(parsedArgs.targetSdkVersion,
887                        parsedArgs.remainingArgs, null /* classLoader */);
888            }

不是zygote进程是新线程就调用了ZygoteInit类的zygoteInit方法,方法内部重要的有:

  1. ZygoteInit.nativeZygoteInit();创建了Binder线程池
  2. RuntimeInit.applicationInit(targetSdkVersion, argv, classLoader);调用RuntimeInit的applicationInit方法

applicationInit返回了findStaticMain方法

   /frameworks/base/core/java/com/android/internal/os/RuntimeInit.java
287    protected static Runnable findStaticMain(String className, String[] argv,
288            ClassLoader classLoader) {
289        Class<?> cl;
290
291        try {
			//反射获取classname的类
292            cl = Class.forName(className, true, classLoader);
293        } catch (ClassNotFoundException ex) {
294            throw new RuntimeException(
295                    "Missing class when invoking static main " + className,
296                    ex);
297        }
298
299        Method m;
300        try {
301            m = cl.getMethod("main", new Class[] { String[].class });
302        } catch (NoSuchMethodException ex) {
303            throw new RuntimeException(
304                    "Missing static main on " + className, ex);
305        } catch (SecurityException ex) {
306            throw new RuntimeException(
307                    "Problem getting static main on " + className, ex);
308        }
309
310        int modifiers = m.getModifiers();
311        if (! (Modifier.isStatic(modifiers) && Modifier.isPublic(modifiers))) {
312            throw new RuntimeException(
313                    "Main method is not public and static on " + className);
314        }
315
316        /*
317         * This throw gets caught in ZygoteInit.main(), which responds
318         * by invoking the exception's run() method. This arrangement
319         * clears up all the stack frames that were required in setting
320         * up the process.
321         */
322        return new MethodAndArgsCaller(m, argv);
323    }
324

先通过反射获取了classname的类,classname是在之前传入的,值为“android.app.ActivityThread”
然后获取ActivityThread的main方法。最后返回new一个MethodAndArgsCaller 对象,
回到之前zygote等待ams请求创建应用程序进程的地方

/frameworks/base/core/java/com/android/internal/os/ZygoteInit.java
				//开启循环,等待ams请求
847            caller = zygoteServer.runSelectLoop(abiList);
848        } catch (Throwable ex) {
849            Log.e(TAG, "System zygote died with exception", ex);
850            throw ex;
851        } finally {
852            zygoteServer.closeServerSocket();
853        }
854
855        // We're in the child process and have exited the select loop. Proceed to execute the
856        // command.
857        if (caller != null) {
858            caller.run();
859        }
860    }

之前说过runSelectLoop经过层层调用会返回一个Runnable类型对象,层层调用之后返回的就是上面的MethodAndArgsCaller 对象传入caller里,最后给caller做了判空就运行了内部实现的run方法,如下

 static class MethodAndArgsCaller implements Runnable {
480        /** method to call */
481        private final Method mMethod;
482
483        /** argument array */
484        private final String[] mArgs;
485
486        public MethodAndArgsCaller(Method method, String[] args) {
487            mMethod = method;
488            mArgs = args;
489        }
490
491        public void run() {
492            try {
493                mMethod.invoke(null, new Object[] { mArgs });
494            } catch (IllegalAccessException ex) {
495                throw new RuntimeException(ex);
496            } catch (InvocationTargetException ex) {
497                Throwable cause = ex.getCause();
498                if (cause instanceof RuntimeException) {
499                    throw (RuntimeException) cause;
500                } else if (cause instanceof Error) {
501                    throw (Error) cause;
502                }
503                throw new RuntimeException(ex);
504            }
505        }
506    }

MethodAndArgsCaller构造方法方法中,把传入的参数给成员变量赋值
内部493行调用了mMethod方法,也就是ActivityThread的main方法。

/frameworks/base/core/java/android/app/ActivityThread.java
6623 public static void main(String[] args) {
6624        Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain");
6625
6626        // CloseGuard defaults to true and can be quite spammy.  We
6627        // disable it here, but selectively enable it later (via
6628        // StrictMode) on debug builds, but using DropBox, not logs.
6629        CloseGuard.setEnabled(false);
6630
6631        Environment.initForCurrentUser();
6632
6633        // Set the reporter for event logging in libcore
6634        EventLogger.setReporter(new EventLoggingReporter());
6635
6636        // Make sure TrustedCertificateStore looks in the right place for CA certificates
6637        final File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId());
6638        TrustedCertificateStore.setDefaultUserDirectory(configDir);
6639
6640        Process.setArgV0("<pre-initialized>");
6641		//创建主线程Looper
6642        Looper.prepareMainLooper();
6643
6644        // Find the value for {@link #PROC_START_SEQ_IDENT} if provided on the command line.
6645        // It will be in the format "seq=114"
6646        long startSeq = 0;
6647        if (args != null) {
6648            for (int i = args.length - 1; i >= 0; --i) {
6649                if (args[i] != null && args[i].startsWith(PROC_START_SEQ_IDENT)) {
6650                    startSeq = Long.parseLong(
6651                            args[i].substring(PROC_START_SEQ_IDENT.length()));
6652                }
6653            }
6654        }
6655        ActivityThread thread = new ActivityThread();
6656        thread.attach(false, startSeq);
6657
6658        if (sMainThreadHandler == null) {
				//创建主线程handle类
6659            sMainThreadHandler = thread.getHandler();
6660        }
6661
6662        if (false) {
6663            Looper.myLooper().setMessageLogging(new
6664                    LogPrinter(Log.DEBUG, "ActivityThread"));
6665        }
6666
6667        // End of event ActivityThreadMain.
6668        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
			//开启消息循环
6669        Looper.loop();
6670
6671        throw new RuntimeException("Main thread loop unexpectedly exited");
6672    }
6673
  移动开发 最新文章
Vue3装载axios和element-ui
android adb cmd
【xcode】Xcode常用快捷键与技巧
Android开发中的线程池使用
Java 和 Android 的 Base64
Android 测试文字编码格式
微信小程序支付
安卓权限记录
知乎之自动养号
【Android Jetpack】DataStore
上一篇文章      下一篇文章      查看所有文章
加:2021-08-01 14:37:31  更:2021-08-01 14:38:30 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/7 10:09:32-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码