Android应用进程启动笔记

Android系统启动后,想要启动一个应用程序,首先需要创建并启动该应用所需的应用程序进程。
AMS在启动应用程序时会检查这个应用程序所需的进程是否存在,不存在就会请求Zygote进程启动相应的进程。
我们知道,Zygote在Java框架层会创建一个Socket的服务端,这个Socket用来等待AMS请求Zygote创建新进程。
Zygote进程通过fork自身创建应用程序进程,这样应用程序进程就获得了Zygote进程在启动时创建的虚拟机,同时还创建了Binder线程池和消息循环。

一、应用程序进程启动过程

应用程序进程创建可以分为两个部分:

  • AMS发送启动应用进程的请求
  • Zygote接收并创建应用程序进程
1、AMS发送创建应用进程的请求

AMS 想要启动应用程序进程,需要向Zygote发送创建应用程序进程的请求。通过AMS的 startProcessLocked方法。

1、AMS & startProcessLocked

    private final void startProcessLocked(ProcessRecord app, String hostingType,
            String hostingNameStr, String abiOverride, String entryPoint, String[] entryPointArgs) {
	
        try {
            try {
                final int userId = UserHandle.getUserId(app.uid);
                AppGlobals.getPackageManager().checkPackageStartable(app.info.packageName, userId);
            } catch (RemoteException e) {
                throw e.rethrowAsRuntimeException();
            }

            int uid = app.uid;
            int[] gids = null;
            int mountExternal = Zygote.MOUNT_EXTERNAL_NONE;
            if (!app.isolated) {
                int[] permGids = null;
                try {
                    checkTime(startTime, "startProcess: getting gids from package manager");
                    final IPackageManager pm = AppGlobals.getPackageManager();
                    permGids = pm.getPackageGids(app.info.packageName,
                            MATCH_DEBUG_TRIAGED_MISSING, app.userId);
                    MountServiceInternal mountServiceInternal = LocalServices.getService(
                            MountServiceInternal.class);
                    mountExternal = mountServiceInternal.getExternalStorageMountMode(uid,
                            app.info.packageName);
                } catch (RemoteException e) {
                    throw e.rethrowAsRuntimeException();
                }

                /*
                 * Add shared application and profile GIDs so applications can share some
                 * resources like shared libraries and access user-wide resources
                 */
                if (ArrayUtils.isEmpty(permGids)) {
                    gids = new int[2];
                } else {
                    gids = new int[permGids.length + 2];
                    System.arraycopy(permGids, 0, gids, 2, permGids.length);
                }
                gids[0] = UserHandle.getSharedAppGid(UserHandle.getAppId(uid));
                gids[1] = UserHandle.getUserGid(UserHandle.getUserId(uid));
            }
            checkTime(startTime, "startProcess: building args");
            if (mFactoryTest != FactoryTest.FACTORY_TEST_OFF) {
                if (mFactoryTest == FactoryTest.FACTORY_TEST_LOW_LEVEL
                        && mTopComponent != null
                        && app.processName.equals(mTopComponent.getPackageName())) {
                    uid = 0;
                }
                if (mFactoryTest == FactoryTest.FACTORY_TEST_HIGH_LEVEL
                        && (app.info.flags&ApplicationInfo.FLAG_FACTORY_TEST) != 0) {
                    uid = 0;
                }
            }
          
            String instructionSet = null;
            if (app.info.primaryCpuAbi != null) {
                instructionSet = VMRuntime.getInstructionSet(app.info.primaryCpuAbi);
            }

            app.gids = gids;
            app.requiredAbi = requiredAbi;
            app.instructionSet = instructionSet;

            // Start the process.  It will either succeed and return a result containing
            if (entryPoint == null) entryPoint = "android.app.ActivityThread";
            checkTime(startTime, "startProcess: asking zygote to start proc");
            Process.ProcessStartResult startResult = Process.start(entryPoint,
                    app.processName, uid, uid, gids, debugFlags, mountExternal,
                    app.info.targetSdkVersion, app.info.seinfo, requiredAbi, instructionSet,
                    app.info.dataDir, entryPointArgs);
            checkTime(startTime, "startProcess: returned from zygote!");
            Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
        ...
        } catch (RuntimeException e) {
            ...
        }
    }

该方法中首先会创建uid和gid,并且给entryPoint赋值android.app.ActivityThread,也就是应用程序主线程类名。最后调用Process.start方法启动进程。

2、 Process & start

    /**
     * Start a new process.
     * @param processClass The class to use as the process's main entry
     *                     point.
     */
    public static final ProcessStartResult start(final String processClass,
                                  final String niceName,
                                  int uid, int gid, int[] gids,
                                  int debugFlags, int mountExternal,
                                  int targetSdkVersion,
                                  String seInfo,
                                  String abi,
                                  String instructionSet,
                                  String appDataDir,
                                  String[] zygoteArgs) {
        try {
            return startViaZygote(processClass, niceName, uid, gid, gids,
                    debugFlags, mountExternal, targetSdkVersion, seInfo,
                    abi, instructionSet, appDataDir, 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方法。

需要注意的是processClass 的值是 android.app.ActivityThread

Process & startViaZygote

    /**
     * Starts a new process via the zygote mechanism.
     */
    private static ProcessStartResult startViaZygote(final String processClass,
                                  final String niceName,
                                  final int uid, final int gid,
                                  final int[] gids,
                                  int debugFlags, int mountExternal,
                                  int targetSdkVersion,
                                  String seInfo,
                                  String abi,
                                  String instructionSet,
                                  String appDataDir,
                                  String[] extraArgs)
                                  throws ZygoteStartFailedEx {
        synchronized(Process.class) {
            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);
      		...
            return zygoteSendArgsAndGetResult(openZygoteSocketIfNeeded(abi), argsForZygote);
        }
    }

该方法主要封装启动参数,最后调用zygoteSendArgsAndGetResult方法

Process & zygoteSendArgsAndGetResult

   /**
     * Sends an argument list to the zygote process, which starts a new child
     * and returns the child's pid. Please note: the present implementation
     * replaces newlines in the argument list with spaces.
     */
    private static ProcessStartResult zygoteSendArgsAndGetResult(
            ZygoteState zygoteState, ArrayList<String> args)
            throws ZygoteStartFailedEx {
        try {
            // Throw early if any of the arguments are malformed. This means we can
            // avoid writing a partial response to the zygote.
            int sz = args.size();
            for (int i = 0; i < sz; i++) {
                if (args.get(i).indexOf('\n') >= 0) {
                    throw new ZygoteStartFailedEx("embedded newlines not allowed");
                }
            }

            /**
             * See com.android.internal.os.ZygoteInit.readArgumentList()
             * Presently the wire format to the zygote process is:
             * a) a count of arguments (argc, in essence)
             * b) a number of newline-separated argument strings equal to count
             *
             * After the zygote process reads these it will write the pid of
             * the child or -1 on failure, followed by boolean to
             * indicate whether a wrapper process was used.
             */
            final BufferedWriter writer = zygoteState.writer;
            final DataInputStream inputStream = zygoteState.inputStream;

            writer.write(Integer.toString(args.size()));
            writer.newLine();

            for (int i = 0; i < sz; i++) {
                String arg = args.get(i);
                writer.write(arg);
                writer.newLine();
            }

            writer.flush();

            // Should there be a timeout on this?
            ProcessStartResult result = new ProcessStartResult();

            // Always read the entire result from the input stream to avoid leaving
            // bytes in the stream for future process starts to accidentally stumble
            // upon.
            result.pid = inputStream.readInt();
            result.usingWrapper = inputStream.readBoolean();

            if (result.pid < 0) {
                throw new ZygoteStartFailedEx("fork() failed");
            }
            return result;
        } catch (IOException ex) {
            zygoteState.close();
            throw new ZygoteStartFailedEx(ex);
        }
    }

该方法的主要作用就是将启动参数argsForZygote 写入ZygoteState中。

ZygoteState是Process的一个静态内部类,结构如下:
在这里插入图片描述
用来表示与Zygote通信的状态。

在执行zygoteSendArgsAndGetResult方法的时候会通过openZygoteSocketIfNeeded获得一个ZygoteState 对象。

	//与服务端的socketName 一致
    public static final String ZYGOTE_SOCKET = "zygote";

    /**
     * Tries to open socket to Zygote process if not already open. If
     * already open, does nothing.  May block and retry.
     */
    private static ZygoteState openZygoteSocketIfNeeded(String abi) throws ZygoteStartFailedEx {
        if (primaryZygoteState == null || primaryZygoteState.isClosed()) {
            try {
            	//与Zygote进程建立socket链接
                primaryZygoteState = ZygoteState.connect(ZYGOTE_SOCKET);
            } catch (IOException ioe) {
                throw new ZygoteStartFailedEx("Error connecting to primary zygote", ioe);
            }
        }
		//匹配ABI
        if (primaryZygoteState.matches(abi)) {
            return primaryZygoteState;
        }

        // The primary zygote didn't match. Try the secondary.
        if (secondaryZygoteState == null || secondaryZygoteState.isClosed()) {
            try {
            secondaryZygoteState = ZygoteState.connect(SECONDARY_ZYGOTE_SOCKET);
            } catch (IOException ioe) {
                throw new ZygoteStartFailedEx("Error connecting to secondary zygote", ioe);
            }
        }

        if (secondaryZygoteState.matches(abi)) {
            return secondaryZygoteState;
        }

        throw new ZygoteStartFailedEx("Unsupported zygote ABI: " + abi);
    }

在该方法中建立与Zygote进程的Socket链接,并匹配ABI后返回ZygoteState 对象。

zygoteSendArgsAndGetResult方法中将argsForZygote写入ZygoteState中,这样Zygote进程就会收到一个创建新应用程序进程的请求。

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

上面我们已经通过Socket向Zygote进程发送一个创建新就应用程序进程的请求。

回到ZygoteInit.javamain()方法中,看看服务端Socket是如何接收请求的。

    public static void main(String argv[]) {
        try {
            boolean startSystemServer = false;
            String socketName = "zygote";
			...
			//创建一个服务端的Socket,socketName 为zygote
            registerZygoteSocket(socketName);
			...
			//预加载类、资源等
            preload();
			...
			//启动SystemServer
            if (startSystemServer) {
                startSystemServer(abiList, socketName);
            }

            //等待AMS请求
            runSelectLoop(abiList);

            closeServerSocket();
        } catch (MethodAndArgsCaller caller) {
            caller.run();
        } catch (Throwable ex) {
            Log.e(TAG, "Zygote died with exception", ex);
            closeServerSocket();
            throw ex;
        }
    }

在main方法中创建服务端Socket后,会执行runSelectLoop方法,等待AMS发起创建进程请求。

1、ZygoteInit & runSelectLoop

    /**
     * Runs the zygote process's select loop. Accepts new connections as
     * they happen, and reads commands from connections one spawn-request's
     * worth at a time.
     *
     * @throws MethodAndArgsCaller in a child process when a main() should
     * be executed.
     */
    private static void runSelectLoop(String abiList) throws MethodAndArgsCaller {
        ArrayList<FileDescriptor> fds = new ArrayList<FileDescriptor>();
        ArrayList<ZygoteConnection> peers = new ArrayList<ZygoteConnection>();

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

        while (true) {
         	StructPollfd[] pollFds = new StructPollfd[fds.size()];
            ...
            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 {
                    boolean done = peers.get(i).runOnce();
                    if (done) {
                        peers.remove(i);
                        fds.remove(i);
                    }
                }
            }
        }
    }

如果服务端的Socket接收到请求的时候,pollFds的size就会大于0,会执行:

boolean done = peers.get(i).runOnce();

即执行 ZygoteConnectionrunOnce()方法。

2、ZygoteConnection & runOnce()

    /**
     * Reads one start command from the command socket. If successful,
     * a child is forked and a {@link ZygoteInit.MethodAndArgsCaller}
     * exception is thrown in that child while in the parent process,
     * the method returns normally. On failure, the child is not
     * spawned and messages are printed to the log and stderr. Returns
     * a boolean status value indicating whether an end-of-file on the command
     * socket has been encountered.
     *
     * @return false if command socket should continue to be read from, or
     * true if an end-of-file has been encountered.
     * @throws ZygoteInit.MethodAndArgsCaller trampoline to invoke main()
     * method in child process
     */
    boolean runOnce() throws ZygoteInit.MethodAndArgsCaller {
        String args[];
        Arguments parsedArgs = null;
        FileDescriptor[] descriptors;
        int pid = -1;
        try {
        	//读取进程启动参数
            args = readArgumentList();
            descriptors = mSocket.getAncillaryFileDescriptors();
        } catch (IOException ex) {
            return true;
        }
		...
        try {
        	//解析启动参数
            parsedArgs = new Arguments(args);
  			...
			//启动进程
            pid = Zygote.forkAndSpecialize(parsedArgs.uid, parsedArgs.gid, parsedArgs.gids,
                    parsedArgs.debugFlags, rlimits, parsedArgs.mountExternal, parsedArgs.seInfo,
                    parsedArgs.niceName, fdsToClose, parsedArgs.instructionSet,
                    parsedArgs.appDataDir);
        } catch (ErrnoException ex) {
            ...
        } 
        try {
            if (pid == 0) {
                // in child
                IoUtils.closeQuietly(serverPipeFd);
                serverPipeFd = null;
                //处理应用程序进程
                handleChildProc(parsedArgs, descriptors, childPipeFd, newStderr);
                return true;
            } else {
                // in parent...pid of < 0 means failure
                IoUtils.closeQuietly(childPipeFd);
                childPipeFd = null;
                return handleParentProc(pid, descriptors, serverPipeFd, parsedArgs);
            }
        } finally {
            IoUtils.closeQuietly(childPipeFd);
            IoUtils.closeQuietly(serverPipeFd);
        }
    }

首先会读取启动进程的参数,并解析到Arguments中。然后调用Zygote.forkAndSpecialize方法创建子进程,返回值为pid,pid的默认值为-1,如果pid为0,则表明当前代码运行在子进程中,执行handleChildProc方法完善子进程创建。

ZygoteConnection & handleChildProc

    private void handleChildProc(Arguments parsedArgs,
            FileDescriptor[] descriptors, FileDescriptor pipeFd, PrintStream newStderr)
            throws ZygoteInit.MethodAndArgsCaller {
		//关闭socket
        closeSocket();
        ZygoteInit.closeServerSocket();
		...
		//设置进程名
        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);
        } else {
            RuntimeInit.zygoteInit(parsedArgs.targetSdkVersion,
                    parsedArgs.remainingArgs, null /* classLoader */);
        }
    }

该方法会关闭子进程的Socket,并给子进程重命名(因为子进程的创建是Zygote进程通过fork自身创建的,因此需要将子进程的socket关闭和重命名)。

最后会执行RuntimeInit.zygoteInit方法

3、RuntimeInit & zygoteInit

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

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

        commonInit();
        //创建Binder线程池
        nativeZygoteInit();
        applicationInit(targetSdkVersion, argv, classLoader);
    }

在该方法中会做一些初始化的操作,并调用nativeZygoteInit()创建Binder线程池。继续查看applicationInit

RuntimeInit & applicationInit

    private static void applicationInit(int targetSdkVersion, String[] argv, ClassLoader classLoader)
            throws ZygoteInit.MethodAndArgsCaller {

        // 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;
        try {
            args = new Arguments(argv);
        } catch (IllegalArgumentException ex) {
            Slog.e(TAG, ex.getMessage());
            // let the process exit
            return;
        }

        // 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
        invokeStaticMain(args.startClass, args.startArgs, classLoader);
    }

在该方法中又会执行invokeStaticMain方法。

其中startClass也就是在开头提到的Process.start方法第一个参数processClass,值为android.app.ActivityThread

RuntimeInit & invokeStaticMain

    private static void invokeStaticMain(String className, String[] argv, ClassLoader classLoader)
            throws ZygoteInit.MethodAndArgsCaller {
        Class<?> cl;

        try {
        	//通过className获得ActivityThread的Class对象
        	//className 为 android.app.ActivityThread
            cl = Class.forName(className, true, classLoader);
        } catch (ClassNotFoundException ex) {
            throw new RuntimeException(
                    "Missing class when invoking static main " + className,
                    ex);
        }

        Method m;
        try {
        	//通过反射获得main方法
            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.
         */
        throw new ZygoteInit.MethodAndArgsCaller(m, argv);
    }

该方法会通过反射获得ActivityThread的main方法,并将它传递给抛出的ZygoteInit.MethodAndArgsCaller异常中。MethodAndArgsCallerZygoteInit静态内部类,并且在ZygoteInit的main方法中捕获该异常。

ZygoteInit & main

    public static void main(String argv[]) {
    	...
        } catch (MethodAndArgsCaller caller) {
            caller.run();
        } catch (Throwable ex) {
            Log.e(TAG, "Zygote died with exception", ex);
            closeServerSocket();
            throw ex;
        }
    }

可以看到,在补货异常的地方会执行MethodAndArgsCaller.run()方法。

4、ZygoteInit 静态内部类 MethodAndArgsCaller & run()

    public static class MethodAndArgsCaller extends Exception
            implements Runnable {
        /** method to call */
        private final Method mMethod;

        /** argument array */
        private final String[] mArgs;

        public MethodAndArgsCaller(Method method, String[] args) {
            mMethod = method;
            mArgs = args;
        }

        public void run() {
            try {
                mMethod.invoke(null, new Object[] { mArgs });
            } catch (IllegalAccessException ex) {
                throw new RuntimeException(ex);
            } catch (InvocationTargetException ex) {
                Throwable cause = ex.getCause();
                if (cause instanceof RuntimeException) {
                    throw (RuntimeException) cause;
                } else if (cause instanceof Error) {
                    throw (Error) cause;
                }
                throw new RuntimeException(ex);
            }
        }
    }

run()方法中会执行mMethod.invoke(),我们已经知道mMethodActivityThreadmain方法,这里通过 反射动态调用 ActivityThread 的 main方法。

二、Binder线程池启动过程

我们知道,在RuntimeInit.zygoteInit()初始化方法中会调用nativeZygoteInit();来创建Binder线程池。

    public static final void zygoteInit(int targetSdkVersion, String[] argv, ClassLoader classLoader)
            throws ZygoteInit.MethodAndArgsCaller {
        commonInit();
        nativeZygoteInit();
        applicationInit(targetSdkVersion, argv, classLoader);
    }
    private static final native void nativeZygoteInit();

nativeZygoteInit()是一个JNI方法,源码如下:
/frameworks/base/include/android_runtime/AndroidRuntime.cpp

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

这里gCurRuntimeAndroidRuntime类型的指针,具体指向的是AndroidRuntime的子类AppRuntime,它在app_main.cpp中定义,AppRuntimeonZygoteInit方法如下:

91    virtual void onZygoteInit()
92    {
93        sp<ProcessState> proc = ProcessState::self();
94        ALOGV("App process: starting thread pool.\n");
95        proc->startThreadPool();
96    }

最后会调用ProcessStatestartThreadPool()启动线程池。

145void ProcessState::startThreadPool()
146{
147    AutoMutex _l(mLock);
148    if (!mThreadPoolStarted) {
149        mThreadPoolStarted = true;
150        spawnPooledThread(true);
151    }
152}

mThreadPoolStarted默认为false,每次启动都会设置为true,通过标记mThreadPoolStarted的值,确保线程池只被启动一次。

最后调用spawnPooledThread方法创建并启动线程池。

300void ProcessState::spawnPooledThread(bool isMain)
301{
302    if (mThreadPoolStarted) {
303        String8 name = makeBinderThreadName();
304        ALOGV("Spawning new pooled thread, name=%s\n", name.string());
305        sp<Thread> t = new PoolThread(isMain);
306        t->run(name.string());
307    }
308}

该方法会创建并启动Binder的线程池PoolThread

PoolThread线程池类是在ProcessState.cpp文件中声明的。

50class PoolThread : public Thread
51{
52public:
53    explicit PoolThread(bool isMain)
54        : mIsMain(isMain)
55    {
56    }
57
58protected:
59    virtual bool threadLoop()
60    {
61        IPCThreadState::self()->joinThreadPool(mIsMain);
62        return false;
63    }
64
65    const bool mIsMain;
66};

可以看到PoolThread继承Thread类,通过调用IPCThreadStatejoinThreadPool(mIsMain) 方法将当前线程注册到Binder驱动中,这样我们创建的线程就加入了Binder线程池中去了。

新创建的应用程序就支持Binder进程间通信了。我们只需要创建当前进程的Binder对象,将它注册到ServerManager中就可以实现Binder进程间通信。

三、消息循环创建过程

在第一节第二部分Zygote接收请求并创建应用程序进程中我们知道,最后会通过反射启动 ActivityThreadmain 方法。

1、ActivityThread & main()

    public static void main(String[] args) {
		...
		//创建主线程的Looper
        Looper.prepareMainLooper();
		//创建ActivityThread
        ActivityThread thread = new ActivityThread();
        thread.attach(false);
		//创建主线程Handler
        if (sMainThreadHandler == null) {
            sMainThreadHandler = thread.getHandler();
        }

        // End of event ActivityThreadMain.
        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
        //循环工作
        Looper.loop();

        throw new RuntimeException("Main thread loop unexpectedly exited");
    }

可以看到,在ActivityThreadmain() 方法中会创建消息循环所需要的Handler、并且绑定主线程的Looper并启动。这样就可以处理消息了。

四、总结

Android应用进程启动主要分为:

  • AMS发送创建进程请求
    1
  • Zygote 接收请求并fork子进程
    2

笔记来源:《Android进阶解密》、袁辉辉Android系统启动系列博客

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值