Zygote 启动应用程序

Zygote 启动应用程序,第1张

文章目录
    • 注册 Zygote 的 socket
    • 请求启动应用
    • 处理启动应用请求
      • fork() 应用进程
      • 子进程初始化
      • 预加载系统类和资源

Zygote 进程初始化完成后,会化身为守护进程来执行启动应用程序的任务。


下面是启动时序图

注册 Zygote 的 socket
  • ZygoteInit 的 main() 方法首先调用 registerServerSocketFromEnv() 来创建一个本地 socket,接着调用 runSelectLoop() 来进入等待 socket 连接的循环中。


  • registerServerSocketFromEnv() 方法如下
    void registerServerSocketFromEnv(String socketName) {
        if (mServerSocket == null) {
            int fileDesc;
            final String fullSocketName = ANDROID_SOCKET_PREFIX + socketName;
            try {
                String env = System.getenv(fullSocketName);
                fileDesc = Integer.parseInt(env);
            } catch (RuntimeException ex) {
                throw new RuntimeException(fullSocketName + " unset or invalid", ex);
            }

            try {
                FileDescriptor fd = new FileDescriptor();
                fd.setInt$(fileDesc);
                mServerSocket = new LocalServerSocket(fd);
                mCloseSocketFd = true;
            } catch (IOException ex) {
                throw new RuntimeException(
                        "Error binding to local socket '" + fileDesc + "'", ex);
            }
        }
    }
registerServerSocketFromEnv 通过设置环境变量 ANDROID_SOCKET_ 来获取 socket句柄。


Zygote 在 init.rc 中定义 init.zygote32(或其他init.zygote32_64等) 有一句话

socket zygote stream 660 root system
Init 进程会通过这条选项来创建一个 AF_UNIX 的 socket,并把它的句柄放到环境变量 ANDROID_SOCKET_zygote 中,这个环境变量后面的 zygote 就是选项中的名字。


得到句柄后将通过 FileDescriptor fd = new FileDescriptor(); 生成一个 FileDescriptor 对象,然后在通过mServerSocket = new LocalServerSocket(fd); 生成一个本地服务 socket,它的值将保存在全局 mServerSocket 中。


请求启动应用
android 启动一个新的进程是在 ActivityManagerService 中完成的,可能会有多种原因导致系统启动一个新的进程,最终在 ActivityManagerService 中都是通过调用方法 startProcessLocked() 来完成这一过程。


startProcessLocked() 准备好参数后,会调用 Process 的 start() 方法来启动应用。


部分代码如下

    private ProcessStartResult startProcess(String hostingType, String entryPoint,
            ProcessRecord app, int uid, int[] gids, int runtimeFlags, int mountExternal,
            String seInfo, String requiredAbi, String instructionSet, String invokeWith,
            long startTime) {
            	// .... 其他代码省略
       		 	// 会调用  Process.start 开启应用
                startResult = Process.start(entryPoint,
                        app.processName, uid, uid, gids, runtimeFlags, mountExternal,
                        app.info.targetSdkVersion, seInfo, requiredAbi, instructionSet,
                        app.info.dataDir, invokeWith,
                        new String[] {PROC_START_SEQ_IDENT + app.startSeq});

				//... 
    }
Process.start() 的第一个参数是String entryPoint,如果往上查找会发现 entryPoint 的定义为 final String entryPoint = "android.app.ActivityThread";  就是应用启动后执行的入口类。


Process.start() 实际上走的是 ZygoteProcess.start() 方法。


方法如下:processClass 就是 android.app.ActivityThread

    public final Process.ProcessStartResult start(final String processClass,
                                                  final String niceName,
                                                  int uid, int gid, int[] gids,
                                                  int runtimeFlags, int mountExternal,
                                                  int targetSdkVersion,
                                                  String seInfo,
                                                  String abi,
                                                  String instructionSet,
                                                  String appDataDir,
                                                  String invokeWith,
                                                  String[] zygoteArgs) {
        try {
            return startViaZygote(processClass, niceName, uid, gid, gids,
                    runtimeFlags, mountExternal, targetSdkVersion, seInfo,
                    abi, instructionSet, appDataDir, invokeWith, false /* startChildZygote */,
                    zygoteArgs);
        } catch (ZygoteStartFailedEx ex) {
            Log.e(LOG_TAG,
                    "Starting VM process through Zygote failed");
            throw new RuntimeException(
                    "Starting VM process through Zygote failed", ex);
        }
    }
接下来通过调用 startViaZygote() 方法,将进程的启动参数保存到argsForZygote,最后调用zygoteSendArgsAndGetResult(openZygoteSocketIfNeeded(abi), argsForZygote);

将应用程序启动的参数发送到 Zygote 进程,startViaZygote 代码如下

private Process.ProcessStartResult startViaZygote(final String processClass,
                                                      final String niceName,
                                                      final int uid, final int gid,
                                                      final int[] gids,
                                                      int runtimeFlags, int mountExternal,
                                                      int targetSdkVersion,
                                                      String seInfo,
                                                      String abi,
                                                      String instructionSet,
                                                      String appDataDir,
                                                      String invokeWith,
                                                      boolean startChildZygote,
                                                      String[] extraArgs)
                                                      throws ZygoteStartFailedEx {
        ArrayList<String> argsForZygote = new ArrayList<String>();

        // --runtime-args, --setuid=, --setgid=,
        // and --setgroups= must go first
        argsForZygote.add("--runtime-args");
        argsForZygote.add("--setuid=" + uid);
        argsForZygote.add("--setgid=" + gid);
        argsForZygote.add("--runtime-flags=" + runtimeFlags);
        if (mountExternal == Zygote.MOUNT_EXTERNAL_DEFAULT) {
            argsForZygote.add("--mount-external-default");
        } else if (mountExternal == Zygote.MOUNT_EXTERNAL_READ) {
            argsForZygote.add("--mount-external-read");
        } else if (mountExternal == Zygote.MOUNT_EXTERNAL_WRITE) {
            argsForZygote.add("--mount-external-write");
        }
        argsForZygote.add("--target-sdk-version=" + targetSdkVersion);

        // --setgroups is a comma-separated list
        if (gids != null && gids.length > 0) {
            StringBuilder sb = new StringBuilder();
            sb.append("--setgroups=");

            int sz = gids.length;
            for (int i = 0; i < sz; i++) {
                if (i != 0) {
                    sb.append(',');
                }
                sb.append(gids[i]);
            }

            argsForZygote.add(sb.toString());
        }

        if (niceName != null) {
            argsForZygote.add("--nice-name=" + niceName);
        }

        if (seInfo != null) {
            argsForZygote.add("--seinfo=" + seInfo);
        }

        if (instructionSet != null) {
            argsForZygote.add("--instruction-set=" + instructionSet);
        }

        if (appDataDir != null) {
            argsForZygote.add("--app-data-dir=" + appDataDir);
        }

        if (invokeWith != null) {
            argsForZygote.add("--invoke-with");
            argsForZygote.add(invokeWith);
        }

        if (startChildZygote) {
            argsForZygote.add("--start-child-zygote");
        }

        argsForZygote.add(processClass);

        if (extraArgs != null) {
            for (String arg : extraArgs) {
                argsForZygote.add(arg);
            }
        }

        synchronized(mLock) {
            return zygoteSendArgsAndGetResult(openZygoteSocketIfNeeded(abi), argsForZygote);
        }
    }
上面代码最后调用了 zygoteSendArgsAndGetResult(openZygoteSocketIfNeeded(abi), argsForZygote) 第一个参数如下调用了openZygoteSocketIfNeeded方法,建立 socket连接并且返回 ZygoteState,对于 LocalSocket 直接通过字符串作为地址进行连接就可以通信。


// 如果socket没有连接,则尝试连接socket,如果已经打开了socket则什么都不做。


private ZygoteState openZygoteSocketIfNeeded(String abi) throws ZygoteStartFailedEx { Preconditions.checkState(Thread.holdsLock(mLock), "ZygoteProcess lock not held"); if (primaryZygoteState == null || primaryZygoteState.isClosed()) { try { primaryZygoteState = ZygoteState.connect(mSocket); } catch (IOException ioe) { throw new ZygoteStartFailedEx("Error connecting to primary zygote", ioe); } maybeSetApiBlacklistExemptions(primaryZygoteState, false); maybeSetHiddenApiAccessLogSampleRate(primaryZygoteState); } if (primaryZygoteState.matches(abi)) { return primaryZygoteState; } // The primary zygote didn't match. Try the secondary. if (secondaryZygoteState == null || secondaryZygoteState.isClosed()) { try { secondaryZygoteState = ZygoteState.connect(mSecondarySocket); } catch (IOException ioe) { throw new ZygoteStartFailedEx("Error connecting to secondary zygote", ioe); } maybeSetApiBlacklistExemptions(secondaryZygoteState, false); maybeSetHiddenApiAccessLogSampleRate(secondaryZygoteState); } if (secondaryZygoteState.matches(abi)) { return secondaryZygoteState; } throw new ZygoteStartFailedEx("Unsupported zygote ABI: " + abi); }

处理启动应用请求
 之前提过,ZygoteInit 类的 main方法最后调用了  zygoteServer.runSelectLoop(abiList); 开始了监听和接收消息的环节。


当 (pollFds[i].revents & POLLIN) == 0 结果 为 false 代表有事件的请求到来,则调用 acceptCommandPeer() 来和客户端建立连接,然后把这个连接添加到监听数组中,等待socket的命令到来。


接到命令后会处理参数,处理结束后会断开连接并且移除监听。


Runnable runSelectLoop(String abiList) {
        ArrayList<FileDescriptor> fds = new ArrayList<FileDescriptor>();
        ArrayList<ZygoteConnection> peers = new ArrayList<ZygoteConnection>();

        fds.add(mServerSocket.getFileDescriptor());
        peers.add(null);

        while (true) {
            StructPollfd[] pollFds = new StructPollfd[fds.size()];
            for (int i = 0; i < pollFds.length; ++i) {
                pollFds[i] = new StructPollfd();
                pollFds[i].fd = fds.get(i);
                pollFds[i].events = (short) POLLIN;
            }
            try {
                Os.poll(pollFds, -1);
            } catch (ErrnoException ex) {
                throw new RuntimeException("poll failed", ex);
            }
            for (int i = pollFds.length - 1; i >= 0; --i) {
                if ((pollFds[i].revents & POLLIN) == 0) {
                    continue;
                }

                if (i == 0) {
                    ZygoteConnection newPeer = acceptCommandPeer(abiList);
                    // 添加到监听数组 等待消息到来
                    peers.add(newPeer);
                    fds.add(newPeer.getFileDesciptor());
                } else {
                    try {
                        ZygoteConnection connection = peers.get(i);
                        // processOneCommand 方法处理读取的命令 处理完后面会断开链接,并从监听列表中移除
                        final Runnable command = connection.processOneCommand(this);

                        if (mIsForkChild) {
                            // We're in the child. We should always have a command to run at this
                            // stage if processOneCommand hasn't called "exec".
                            if (command == null) {
                                throw new IllegalStateException("command == null");
                            }

                            return command;
                        } else {
                            // We're in the server - we should never have any commands to run.
                            if (command != null) {
                                throw new IllegalStateException("command != null");
                            }

                            // We don't know whether the remote side of the socket was closed or
                            // not until we attempt to read from it from processOneCommand. This shows up as
                            // a regular POLLIN event in our regular processing loop.
                            // 判断是否处理完了事件,处理完则断开链接 移除监听
                            if (connection.isClosedByPeer()) {
                                connection.closeSocket();
                                peers.remove(i);
                                fds.remove(i);
                            }
                        }
                    }
                    // 。






} } }

fork() 应用进程
ZygoteConnnect 类中的 processOneCommand 负责创建子进程,首先调用args = readArgumentList(); 从 socket 中读入多个参数,参数样式为 “--setuid=1” 行与行之间用 /r /n 分割,读取完成后传入新建的 Arguments 对象 并调用parseArgs(args);解析成参数列表。


解析参数之后还会对参数进行校验和设置。


		// 检查用户是否有权利指定进程用户id,组id和所属的组,以及一些安全检查等。


applyUidSecurityPolicy(parsedArgs, peer); applyInvokeWithSecurityPolicy(parsedArgs, peer); applyDebuggerSystemProperty(parsedArgs); applyInvokeWithSystemProperty(parsedArgs);

上面检测通过后,processOneCommand 会调用下面代码来 fork 子进程
 pid = Zygote.forkAndSpecialize(parsedArgs.uid, parsedArgs.gid, parsedArgs.gids,
                parsedArgs.runtimeFlags, rlimits, parsedArgs.mountExternal, parsedArgs.seInfo,
                parsedArgs.niceName, fdsToClose, fdsToIgnore, parsedArgs.startChildZygote,
                parsedArgs.instructionSet, parsedArgs.appDataDir);

最终会调用 native 层的 nativeForkAndSpecialize() 的函数来完成 fork  *** 作
native private static int nativeForkAndSpecialize(int uid, int gid, int[] gids,int runtimeFlags,
          int[][] rlimits, int mountExternal, String seInfo, String niceName, int[] fdsToClose,
          int[] fdsToIgnore, boolean startChildZygote, String instructionSet, String appDataDir);
nativeForkAndSpecialize() 函数的主要工作:
- fork() 出子进程
- 在子进程中挂载 external storage
- 在子进程中设置用户 Id、组Id、所属进程id
- 在子进程中执行系统调用 setrlimit 来设置系统资源限制
- 在子进程中执行系统调用 capset 来设置进程的权限
- 在子进程中设置应用上下文

native 返回 Java 层后,processOneCommand 方法继续执行,如果 pid==0 则在父进程中,否则在子进程中。


			if (pid == 0) {
                // in child
                zygoteServer.setForkChild();

                zygoteServer.closeServerSocket();
                IoUtils.closeQuietly(serverPipeFd);
                serverPipeFd = null;

                return handleChildProc(parsedArgs, descriptors, childPipeFd,
                        parsedArgs.startChildZygote);
            } else {
                // In the parent. A pid < 0 indicates a failure and will be handled in
                // handleParentProc.
                IoUtils.closeQuietly(childPipeFd);
                childPipeFd = null;
                handleParentProc(pid, descriptors, serverPipeFd);
                return null;
            }
子进程初始化
上面讲了如果是在子进程中,则执行 handleChildProc() 函数。


来完成子进程的初始化,代码如下

private Runnable handleChildProc(Arguments parsedArgs, FileDescriptor[] descriptors,
            FileDescriptor pipeFd, boolean isZygote) {
        /**
         * By the time we get here, the native code has closed the two actual Zygote
         * socket connections, and substituted /dev/null in their place.  The LocalSocket
         * objects still need to be closed properly.
         */
		// 首先关闭了 socket 连接
        closeSocket();
        if (descriptors != null) {
            try {
                Os.dup2(descriptors[0], STDIN_FILENO);
                Os.dup2(descriptors[1], STDOUT_FILENO);
                Os.dup2(descriptors[2], STDERR_FILENO);

                for (FileDescriptor fd: descriptors) {
                    IoUtils.closeQuietly(fd);
                }
            } catch (ErrnoException ex) {
                Log.e(TAG, "Error reopening stdio", ex);
            }
        }

        if (parsedArgs.niceName != null) {
            Process.setArgV0(parsedArgs.niceName);
        }

        // End of the postFork event.
        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
        if (parsedArgs.invokeWith != null) {
            WrapperInit.execApplication(parsedArgs.invokeWith,
                    parsedArgs.niceName, parsedArgs.targetSdkVersion,
                    VMRuntime.getCurrentInstructionSet(),
                    pipeFd, parsedArgs.remainingArgs);

            // Should not get here.
            throw new IllegalStateException("WrapperInit.execApplication unexpectedly returned");
        } else {
            if (!isZygote) {
                return ZygoteInit.zygoteInit(parsedArgs.targetSdkVersion, parsedArgs.remainingArgs,
                        null /* classLoader */);
            } else {
                return ZygoteInit.childZygoteInit(parsedArgs.targetSdkVersion,
                        parsedArgs.remainingArgs, null /* classLoader */);
            }
        }
    }
parsedArgs.invokeWith 普遍为Null,如果不为 Null 则通过 exec 方式启动 app_process 进程来执行Java类(通过执行 exec 的区别前面文章有讲过,fork + exec 和 单独 fork 不执行 exec 的区别)。


正常情况会调用 ZygoteInit.zygoteInit ,主要执行的方法 是 RuntimeInit.commonInit,ZygoteInit.nativeZygoteInit() 和 RuntimeInit.applicationInit

    public static final Runnable zygoteInit(int targetSdkVersion, String[] argv, ClassLoader classLoader) {
        if (RuntimeInit.DEBUG) {
            Slog.d(RuntimeInit.TAG, "RuntimeInit: Starting application from zygote");
        }

        Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ZygoteInit");
        RuntimeInit.redirectLogStreams();

        RuntimeInit.commonInit();
        ZygoteInit.nativeZygoteInit();
        return RuntimeInit.applicationInit(targetSdkVersion, argv, classLoader);
    }
  • RuntimeInit.commonInit(); 方法
protected static final void commonInit() {
        LoggingHandler loggingHandler = new LoggingHandler();
        // 设置 UncaughtException 的处理方法
        Thread.setUncaughtExceptionPreHandler(loggingHandler);
        Thread.setDefaultUncaughtExceptionHandler(new KillApplicationHandler(loggingHandler));
		// 设置时区
        TimezoneGetter.setInstance(new TimezoneGetter() {
            @Override
            public String getId() {
                return SystemProperties.get("persist.sys.timezone");
            }
        });
        TimeZone.setDefault(null);
        // 重制 Log 系统
        LogManager.getLogManager().reset();
        new AndroidConfig();
        // 设置 http.agent 属性
        String userAgent = getDefaultUserAgent();
        System.setProperty("http.agent", userAgent);
        NetworkManagementSocketTagger.install();
        String trace = SystemProperties.get("ro.kernel.android.tracing");
        if (trace.equals("1")) {
            Slog.i(TAG, "NOTE: emulator trace profiling enabled");
            Debug.enableEmulatorTraceOutput();
        }

        initialized = true;
    }
  • ZygoteInit.nativeZygoteInit(); 方法
    这个是 native 方法 private static final native void nativeZygoteInit(); 调用的是 AndroidRuntime.cpp中的方法

static void com_android_internal_os_ZygoteInit_nativeZygoteInit(JNIEnv* env, jobject clazz)
{
    gCurRuntime->onZygoteInit();
}

gCurRuntime 是 AppRuntime 的全局指针,它的 onZygoteInit() 函数如下:主要是初始化了 Binder 环境,这样应用就可以使用 Binder了。


    virtual void onZygoteInit()
    {
        sp<ProcessState> proc = ProcessState::self();
        ALOGV("App process: starting thread pool.\n");
        proc->startThreadPool();
    }
  • RuntimeInit.applicationInit() 方法
protected static Runnable applicationInit(int targetSdkVersion, String[] argv,
            ClassLoader classLoader) {
        // If the application calls System.exit(), terminate the process
        // immediately without running any shutdown hooks.  It is not possible to
        // shutdown an Android application gracefully.  Among other things, the
        // Android runtime shutdown hooks close the Binder driver, which can cause
        // leftover running threads to crash before the process actually exits.
        nativeSetExitWithoutCleanup(true);
	
        // We want to be fairly aggressive about heap utilization, to avoid
        // holding on to a lot of memory that isn't needed.
        // 这个两个是设置虚拟机的参数
        VMRuntime.getRuntime().setTargetHeapUtilization(0.75f);
        VMRuntime.getRuntime().setTargetSdkVersion(targetSdkVersion);

        final Arguments args = new Arguments(argv);

        // The end of of the RuntimeInit event (see #zygoteInit).
        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);

        // Remaining arguments are passed to the start class's static main
        // findStaticMain 内部的 Run 方法通过 invoke 执行Java层的 main 方法
        return findStaticMain(args.startClass, args.startArgs, classLoader);
    }
findStaticMain 经过一系列的处理最后会调用到 MethodAndArgsCaller 返回 Runnable,执行run方法后执行的是下面方法,mMethod 实际上是 main 方法。



 // Method m;
 //       try {
 //           m = cl.getMethod("main", new Class[] { String[].class });

 mMethod.invoke(null, new Object[] { mArgs });
接下来就进入了应用本身的启动流程了。


预加载系统类和资源
为了加快应用程序的启动,Android把系统常用的 Java 类和一部分 Framework 的资源在 zygote 进程中加载,这些预加载的类和资源在所有经 Zygote fork出的进程中都是共享的。


ZygoteInit 在 main() 方法中调用的 preload(bootTimingsTraceLog); 加载系统类,系统资源和OpenGL等方法初始化如下
static void preload(TimingsTraceLog bootTimingsTraceLog) {
        beginIcuCachePinning();
      
        preloadClasses();
      
        preloadResources();
     
        nativePreloadAppProcessHALs();
      
        preloadOpenGL();
       
        preloadSharedLibraries();
        preloadTextResources();
        
        WebViewFactory.prepareWebViewInZygote();
        endIcuCachePinning();
        warmUpJcaProviders();
        sPreloadComplete = true;
    }
  • 预加载 Java 类

    系统的 Java 类会频繁的执行,因此,需要把类的信息预先加载到系统内存中,并在所有应用进程中共享,Android将所有需要预加载的类放到了本地文件 preloaded-classes中,android9的目录位于 frameworks/base/config/preloaded-classes

android.R$styleable
android.accessibilityservice.AccessibilityServiceInfo
android.accessibilityservice.AccessibilityServiceInfo$1
android.accounts.Account
android.accounts.Account$1
android.accounts.AccountManager
android.accounts.AccountManager$1
android.accounts.AccountManager$10
android.accounts.AccountManager$11
android.accounts.AccountManager$18
android.accounts.AccountManager$2
android.accounts.AccountManager$20
android.accounts.IAccountManagerResponse
android.accounts.IAccountManagerResponse$Stub
android.accounts.OnAccountsUpdateListener
android.accounts.OperationCanceledException
android.animation.AnimationHandler
android.animation.AnimationHandler$1
android.animation.AnimationHandler$AnimationFrameCallback
android.animation.AnimationHandler$AnimationFrameCallbackProvider

.....
加载这些类的方法是 preloadClasses() ,流程如下
    private static void preloadClasses() {
        final VMRuntime runtime = VMRuntime.getRuntime();
		// 首先获取一个 FileInputStream 通过流读取文件内容
        InputStream is;
        try {
            is = new FileInputStream(PRELOADED_CLASSES);
        } catch (FileNotFoundException e) {
            Log.e(TAG, "Couldn't find " + PRELOADED_CLASSES + ".");
            return;
        }
		// 。








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 { if (false) { Log.v(TAG, "Preloading " + line + "..."); } // 然后通过 Class.forName 装载类的信息(它不会创建一个对象) Class.forName(line, true, null); count++; } //。








}

  • 预加载资源
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();
                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();
                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();
                    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);
        }
    }

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/584484.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-12
下一篇 2022-04-12

发表评论

登录后才能评论

评论列表(0条)

保存