android源码学习- APP启动流程(android12源码)

这篇具有很好参考价值的文章主要介绍了android源码学习- APP启动流程(android12源码)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

前言:

百度一搜能找到很多讲APP启动流程的,但是往往要么就是太老旧(还是基于android6去分析的),要么就是不全(往往只讲了整个流程的一小部分)。所以我结合网上现有的文章,以及源码的阅读和调试,耗费了3整天的时间,力求写出一篇最完整,最详细,最通俗易懂的文章,来讲清楚在android上,APP是如何启动的。

 该文属于安卓源码探究专栏中的文章,专栏中很多类似源码分析的文章,欢迎大家阅读。

链接如下:

安卓源码研究

一、APP启动流程概览

涉及到四个进程之间的通信,分别是Laucher进程(桌面APP),SystemServer进程(AMS所属进程),Zygote进程(系统和所有APP的创建进程),APP进程。

APP简要的启动流程是这样的:
1.Laucher进程会通过binder的方式通知SystemServer进程。

2.然后SystemServer进程中的AMS会查询对应的Activity栈信息,如果对应APP进程不存在则会加载占位图。然后通过socket的方式通知Zygote去创建APP进程。

3.APP进程创建后会执行main方法,然后通知AMS

4.AMS收到信息后会继续通知APP去创建Application,并且接下来会通知APP进程取拉起Activity。

5.APP进程依次收到通知后,会依次完成加载APK,初始化Application,执行Activtiy生命周期等操作。最终会把首屏展示出来。

流程图如下,建议双击放大后观看:

android源码学习- APP启动流程(android12源码)

 

接下来的几章,会按照上面的流程逐一拆解分析:

二、Launcher通知AMS启动APP

Launcher进程其实和普通APP是一样的,甚至我们可以把自己的APP设置为桌面APP。

2.1 Launcher获取到AMS的binder

而Launcher通知AMS的流程和正常APP也是一样的,通过ServiceManager获取到AMS的binder引用。这里提到了ServiceManager,其实ServiceManagerService也是单独的一个进程,其存储了所有APP所需要的binder引用。而且其地址是固定的,所以获取ServiceManager可以直接获取。

2.2 通知ActivityManagerService

Launcher通过上述获取到的binder通知到AMS,调用的方式是startActivityWithFeature。

而由于AMS实现了IActivityManager.Stub的实现,所以其startActivityWithFeature方法会收到launcher发过来的通知。

2.3 系统进程加载占位图

AMS中,会交由ActivityTaskManagerService去进行对应启动任务的处理。最终会交给ActivityStart进行处理。

ActivityStart中,首先会进行一个逻辑判断,如果进程不存在,则首先会加载APP中MainActivity的主题作为背景图,显示到屏幕上。这一步操作是发生在SystemServer进程的,APP进程还未创建。

android11开始支持启动动画,逻辑也是在这里处理的。

2.4 AMS进行启动操作

一个进程中会有多个任务栈,栈对应的是Task类。

一个任务栈中会有多个Activity对象,这个Activity对象在AMS中使用ActivityRecord记录。

这一块的具体逻辑,我会单独写一篇Activity的启动逻辑来进行描述。

这里暂时先简单描述下,如果AMS发现进程不存在,会去通知Zygote的进行进程fork就好,对应的fork逻辑在下一章。

三、Zygote创建APP进程

3.1 AMS中内部逻辑执行

ActivityTaskManagerService调用startProcessAsync方法会去负责创建APP进程,这是异步的,通过handler转发后最终会调用到LocalService.startProcess()方法。

然后会通知到ProcessList.startProcessLocked方法,这个方法中,会构造一个对象ProcessRecord对象,然后把各种信息添加到这个对象中。

接下来会调用到ProcessList.startProcessLocked这个方法,这个方法主要是负责把各种信息转换为runtimeFlags标记位,连同上面构造的ProcessRecord继续传入下一层。

最终通知到Process.start方法,然后交由ZygoteProcess.start方法,最终传递到startViaZygote方法。

3.2 请求参数拼接成字符串发送给Zygote

startViaZygote这个方法中,会把各种配置参数拼接为字符串。

最终在ZygoteProcess.attemptUsapSendArgsAndGetResult方法中,通过LocalSocket的方式把上面拼接的字符串内容传递给Zygote进程。并且从socket中读取返回值,返回值的PID>0则证明进程创建成功。

3.3 Zygote进程逻辑

在了解Zygote进程如何解析AMS发送过来的请求之前,我们先简单了解下Zygote进程创建后的一些基本逻辑。如下图:

android源码学习- APP启动流程(android12源码)

一。Zygote进程创建后,最开始的入口是在C层,app_main.cpp文件的main方法(该进程由Init进程启动,这里就不扩展了),在main方法中会配置一些JVM的参数,这个和JAVA的虚拟机参数配置类似。最终会调用到AndroidRuntime.cpp中的start函数,去启动JVM虚拟机。

 if (startVm(&mJavaVM, &env, zygote, primary_zygote) != 0) {
        return;
    }

二。虚拟机创建之后,会通过native层反射找到main函数,并调用ZygoteInit类的main函数。

AndroidRumtime.cpp的start方法中:

if (startClass == NULL) {
        ALOGE("JavaVM unable to locate class '%s'\n", slashClassName);
        /* keep going */
    } else {
        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 {
            env->CallStaticVoidMethod(startClass, startMeth, strArray);

#if 0
            if (env->ExceptionCheck())
                threadExitUncaughtException(env);
#endif
        }
    }

三。ZygoteInit类的main方法中,会做如下几件事:

第一步,把自身的进程ID设置为0,并且没有parent进程。

第二步,会进行一系列的初始化操作,比如加载native环境,加载JVM环境,加载系统类,加载系统资源等等。都在其preload方法中。

第三步,如果是首次执行,则会创建SystemServer进程。这也是Zygote进程的大儿子。AMS,WMS都属于SystemServer进程。

第四步,会创建zygoteServer对象,并且调用其runSelectLoop方法。监听socket不断的接传递过来的信息

第五步,Zygote进程的fork,其实是复制一个原原本本的自己。runSelectLoop方法中其实会去执行fork操作,这个后面会讲,我们这里只需要知道,执行到caller.run();这一句的时候,已经处于APP进程状态了。

public static void main(String[] argv) {
        ZygoteServer zygoteServer = null;

        // Mark zygote start. This ensures that thread creation will throw
        // an error.
        ZygoteHooks.startZygoteNoThreadCreation();

        // Zygote goes into its own process group.
        try {
            Os.setpgid(0, 0);
        } catch (ErrnoException ex) {
            throw new RuntimeException("Failed to setpgid(0,0)", ex);
        }

        
            
        ...
         
            if (!enableLazyPreload) {
                bootTimingsTraceLog.traceBegin("ZygotePreload");
                EventLog.writeEvent(LOG_BOOT_PROGRESS_PRELOAD_START,
                        SystemClock.uptimeMillis());
                //2.初始化操作
                preload(bootTimingsTraceLog);
                EventLog.writeEvent(LOG_BOOT_PROGRESS_PRELOAD_END,
                        SystemClock.uptimeMillis());
                bootTimingsTraceLog.traceEnd(); // ZygotePreload
            }

            // Do an initial gc to clean up after startup
            bootTimingsTraceLog.traceBegin("PostZygoteInitGC");
            gcAndFinalize();
            bootTimingsTraceLog.traceEnd(); // PostZygoteInitGC

            bootTimingsTraceLog.traceEnd(); // ZygoteInit

            Zygote.initNativeState(isPrimaryZygote);

            ZygoteHooks.stopZygoteNoThreadCreation();

            zygoteServer = new ZygoteServer(isPrimaryZygote);

            if (startSystemServer) {
                //3.首次启动时,会启动系统进程
                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) {
                    r.run();
                    return;
                }
            }

             //4.启动无限循环监听socket
            caller = zygoteServer.runSelectLoop(abiList);
        } catch (Throwable ex) {
            Log.e(TAG, "System zygote died with fatal exception", ex);
            throw ex;
        } finally {
            if (zygoteServer != null) {
                zygoteServer.closeServerSocket();
            }
        }

        // We're in the child process and have exited the select loop. Proceed to execute the
        // command.
        if (caller != null) {
            //5.这里的调用已经是在APP进程了,zygote进程永远不会执行到这里
            caller.run();
        }
    }

3.4 收到通知后去fork产生APP进程

runSelectLoop方法中,会开启一个无限循环。如果收到了消息

如果收到了消息,则会调用ZygoteConnection.processCommand去处理。

Runnable runSelectLoop (String abiList){
//        ...

            while (true) {
//            ...
                try {
                    ZygoteConnection connection = peers.get(pollIndex);
                    boolean multipleForksOK = !isUsapPoolEnabled()
                            && ZygoteHooks.isIndefiniteThreadSuspensionSafe();
                    //收到消息,处理消息并且返回runnable。
                    final Runnable command =
                            connection.processCommand(this, multipleForksOK);

                    // TODO (chriswailes): Is this extra check necessary?
                    if (mIsForkChild) {
                        if (command == null) {
                            throw new IllegalStateException("command == null");
                        }
                         //子进程执行,子进程的mIsForkChild会被设置为true,则返回command
                        return command;
                    } else {
                         //Zygote进程执行,则继续执行循环
                        //       ...
                    }
                }
            }
        }

而在processCommand中,会解析收到的参数,最终调用Zygote.forkAndSpecialize去fork一个新进程。这个方法虽然只会调用一次,返回因为进程是拷贝的,所以实际上会有两次返回,返回两个pid。pid为0时为子进程,设置标记为子进程。反之仍就还是Zygote进程。

 pid = Zygote.forkAndSpecialize(parsedArgs.mUid, parsedArgs.mGid,
                            parsedArgs.mGids, parsedArgs.mRuntimeFlags, rlimits,
                            parsedArgs.mMountExternal, parsedArgs.mSeInfo, parsedArgs.mNiceName,
                            fdsToClose, fdsToIgnore, parsedArgs.mStartChildZygote,
                            parsedArgs.mInstructionSet, parsedArgs.mAppDataDir,
                            parsedArgs.mIsTopApp, parsedArgs.mPkgDataInfoList,
                            parsedArgs.mAllowlistedDataInfoList, parsedArgs.mBindMountAppDataDirs,
                            parsedArgs.mBindMountAppStorageDirs);

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

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

                            return handleChildProc(parsedArgs, childPipeFd,
                                    parsedArgs.mStartChildZygote);
                        } else {
                            // In the parent. A pid < 0 indicates a failure and will be handled in
                            // handleParentProc.
                            IoUtils.closeQuietly(childPipeFd);
                            childPipeFd = null;
                            handleParentProc(pid, serverPipeFd);
                            return null;
                        }
                    } finally {
                        IoUtils.closeQuietly(childPipeFd);
                        IoUtils.closeQuietly(serverPipeFd);
                    }

3.5 调用ActivityThread.main方法

调用到handleChildProc方法时,已经处于APP进程的状态。

该方法掉调用到ZygoteInit.zygoteInit()方法,相关代码如下:

    public static Runnable zygoteInit(int targetSdkVersion, long[] disabledCompatChanges,
            String[] argv, ClassLoader classLoader) {
        ...
        RuntimeInit.redirectLogStreams();
        RuntimeInit.commonInit();
        ZygoteInit.nativeZygoteInit();
        return RuntimeInit.applicationInit(targetSdkVersion, disabledCompatChanges, argv,
                classLoader);
    }

第一步,把System.out的输出重定向到Logcat中;

第二步,在commonInit方法中,设置Thread的UncaughtExceptionPreHandler和DefaultUncaughtExceptionHandler。用于应用发生异常时的处理,这里稍微扩展下,DefaultUncaughtExceptionHandler设置的是RuntimeInit.KillApplicationHandler,所以所有最终未处理的异常都会走到这个类中。

第三步,native中进行相关的初始化。

最后一步,做VM虚拟机的一些配置,然后就会调用findStaticMain方法。

接下来,我们看下findStaticMain方法:

protected static Runnable findStaticMain(String className, String[] argv,
            ClassLoader classLoader) {
        Class<?> cl;

        try {
            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);
    }

这里只看到出现了main方法,并没有看到ActivityThread类的声明。所以,是如何最终选择启动类类中的main方法呢?其实原理很简单,这个方法中有一个className参数,这个其实就是ActivityThread类,它是通过socket传递过来的一个参数,其定义在ProcessList的startProcessLocked方法中

所以最后返回的其实是一个runnable接口实现类,而这个runnable中实现了调用ActivityThread中main方法的逻辑。

还记得3.3中Zygote创建后的逻辑吗?最后一句是调用 caller.run();

是的,这个caller就是最后返回的runnable接口实现类,去完成调用main方法的操作。

四、APP进程创建初始化操作

app进程初始化的操作比较简单,主要做了两件事,初始化looper,以及通知AMS。(注意,这里只是APP进程创建了,但是还没有加载APK中的任何类)

4.1 初始化MainLooper

调用main方法的线程,会被设置为主线程,Looper此时会开启无限循环。

main(){
    //looper绑定主线程
    Looper.prepareMainLooper();

    //通知AMS
    ActivityThread thread = new ActivityThread();
    thread.attach(false, startSeq);

    //looper开启无限循环读取消息
    Looper.loop();
}

4.2 通知AMS

ActivityThread.attach方法中,实现逻辑也是比较简单的,直接通过binder通知AMS,并且把自身的binder(ApplicationThread)也传递给AMS。

            final IActivityManager mgr = ActivityManager.getService();
            try {
                mgr.attachApplication(mAppThread, startSeq);
            } catch (RemoteException ex) {
                throw ex.rethrowFromSystemServer();
            }

这里AMS的binder是直接通过ServierManager的方式获取的。ServierManager存储了所有的binder引用,注册的形式,AMS在启动的时候去注册。

五、AMS通知APP进程进行各种生命周期操作

5.1 唤起APP初始化并拉起APP首屏

ActivityManagerService的attachApplication方法会收到APP传递过来的消息,然后交由attachApplicationLocked处理。

attachApplicationLocked中主要负责两件事:

1.通知APP进程进行初始化操作;

2.进行一些列操作,最终通知APP拉起指定的MainActivity。

说到这,问一个问题1,为什么明明是串行通知APP去执行的,而APP那边不会出现先加载Activity,再去初始化应用的情况呢?答案在下一小节。

 @GuardedBy("this")
    private boolean attachApplicationLocked(@NonNull IApplicationThread thread,
            int pid, int callingUid, long startSeq) {
        ...
            if (app.getIsolatedEntryPoint() != null) {
                ...
            } else if (instr2 != null) {
                //1通知APP进行初始化操作
                thread.bindApplication(processName, appInfo, providerList,
                        instr2.mClass,
                        profilerInfo, instr2.mArguments,
                        instr2.mWatcher,
                        instr2.mUiAutomationConnection, testMode,
                        mBinderTransactionTrackingEnabled, enableTrackAllocation,
                        isRestrictedBackupMode || !normalMode, app.isPersistent(),
                        new Configuration(app.getWindowProcessController().getConfiguration()),
                        app.getCompat(), getCommonServicesLocked(app.isolated),
                        mCoreSettingsObserver.getCoreSettingsLocked(),
                        buildSerial, autofillOptions, contentCaptureOptions,
                        app.getDisabledCompatChanges(), serializedSystemFontMap);
            } else {
                ...
                同上
            }
        ...
        // See if the top visible activity is waiting to run in this process...
        if (normalMode) {
            try {
                //拉起
                didSomething = mAtmInternal.attachApplication(app.getWindowProcessController());
            } catch (Exception e) {
                Slog.wtf(TAG, "Exception thrown launching activities in " + app, e);
                badApp = true;
            }
        }
        ...
        return true;
    }

5.2 APP进行初始化操作

ActivityThread中ApplicationThread的bindApplication会收到通知,通过handler交给主线程去处理。所以我们也就知道上面问题1的答案了,无论是初始化APP,还是拉起Activity,都是最终交给Handler切换到主线程处理的。所以哪怕初始化APP是耗时操作,拉起Activity的任务也得排队等到前面任务执行完了才能执行。

最终通过handler是交给handleBindApplciation去完成APP的初始化逻辑的。主要包含下面几个操作:

1.使用classLoader去加载APK中的DEX文件。

2.加载APK中的资源。

3.反射生成Application类,并调用其attachBaseApplication方法。

4.调用Application的onCreate方法。

@UnsupportedAppUsage
    private void handleBindApplication(AppBindData data) {
        //1.classLoader加载APK中的dex,并且加载APK的资源
        final ContextImpl appContext = ContextImpl.createAppContext(this, data.info);

        // Continue loading instrumentation.
        //2.生成代理类
        mInstrumentation = new Instrumentation();
        mInstrumentation.basicInit(this);
        // Allow disk access during application and provider setup. This could
        // block processing ordered broadcasts, but later processing would
        // probably end up doing the same disk access.
        Application app;
        //3.声明application
        app = data.info.makeApplication(data.restrictedBackupMode, null);
            ...
        //4.调用Application的onCreate
        mInstrumentation.onCreate(data.instrumentationArgs);

    }

onCreate创建完成后并不会通知AMS,因为activity的拉起操作和初始化Application在AMS中是串行的。

5.3 AMS中处理Activity启动逻辑

5.1中讲到attachApplicationLocked会最终通知APP去拉起Activity,那么整个流程是怎样的呢?

会执行下面这样的调用顺序。

ActivityManagerService.attachApplicationLocked->

ActivityTaskManagerService.LocalService.attachApplication->

RootWindowContainer.attachApplication->

RootWindowContainer.startActivityForAttachedApplicationIfNeeded->

ActivityTaskSupervisor.realStartActivityLocked

到了realStartActivityLocked这一步,正好对应2.4所讲的。APP进程存在的也会调用这个方法,而不存在则先创建进程,最终也会执行到这一步。

realStartActivityLocked中创建Activity的生命周期事务,最终通过ClientLifecycleManager.scheduleTransaction通过binder发送到APP进程的ApplicationThread.scheduleTransaction方法中,则AMS流程就完成了。

5.4 APP完成Activity的启动

生命周期事务是安卓8.0之后出现的,简单来说就是之前的模式是:

AMS发一个协商好消息,APP收到后,根据消息内存来决定自己去做操作;

而事务模式下,AMS发送一系列事务到APP进程,APP收到后,直接去执行这一系列的事务。而这些事务就是activity的生命周期调用。文章来源地址https://www.toymoban.com/news/detail-507886.html

到了这里,关于android源码学习- APP启动流程(android12源码)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【Android12】Monkey压力测试源码执行流程分析

    Monkey是Android提供的用于应用程序自动化测试、压力测试的测试工具。 其源码路径(Android12)位于 部署形式为Java Binary 通过Monkey,可以模拟用户的Touch(单指、多指、手势)、按键(key)事件等,检测应用程序发生的ANR、Crash事件,并收集相关Debug信息等。 例如测试应用com.packa

    2024年03月22日
    浏览(43)
  • Activity启动过程详解(Android 12源码分析)

    启动一个Activity,通常有两种情况,一种是在应用内部启动Activity,另一种是Launcher启动 1、应用内启动 通过startActivity来启动Activity 启动流程: 一、Activity启动的发起 二、Activity的管理——ATMS 三、线程切换即消息处理——mH 四、Activity启动核心实现——初始化及生命周期 2、

    2024年02月13日
    浏览(48)
  • Android S从桌面点击图标启动APP流程 (六)

    系列文章 Android S从桌面点击图标启动APP流程 (一) Android S从桌面点击图标启动APP流程 (二) Android S从桌面点击图标启动APP流程 (三) Android S从桌面点击图标启动APP流程 (四) Android S从桌面点击图标启动APP流程 (五) Android 12的源码链接: android 12 aosp http://aospxref.com/android-12.0.0_r3/ 上文

    2024年02月06日
    浏览(47)
  • Android系统启动流程 源码解析

    本文链接:https://blog.csdn.net/feather_wch/article/details/132518105 有道云脑图:https://note.youdao.com/s/GZ9d8vzO 1、整体流程 Boot Room BootLoader idle kthread init init ServiceManager zygote zygote SystemServer app 1、kernel/common/init/main.c 2、andorid.mk-android.bp编译 3、init是用户空间鼻祖 属于C、C++ Framework 1.1 启动源

    2024年02月11日
    浏览(51)
  • 【Android12】Android Framework系列---Adb和PMS安装apk源码流程

    通过adb install命令可以将apk安装到Android系统(注意:特定类型的apk,比如persist类型是无法通过adb安装的) 下述命令中adb解析install命令,并调用Android PackageManagerService进行apk安装。 基于Android12,分析从adb install到 PakcageManagerService安装apk的流程。 adb install命令的源码实现 Andro

    2024年01月22日
    浏览(62)
  • Android 12 源码分析 —— 应用层 二(SystemUI大体组织和启动过程)

    在前一篇文章中,我们介绍了SystemUI怎么使用IDE进行编辑和调试。这是分析SystemUI的最基础,希望读者能尽量掌握。 本篇文章,将会介绍SystemUI的大概组织架构,以及它的启动过程。本篇文章读完,将会知道: SystemUI为什么选择使用Dagger2 SystemUI怎么新建一个模块 SystemUI的启动

    2024年02月06日
    浏览(80)
  • 【Android】APP启动优化学习笔记

    用户体验: 应用的启动速度直接影响用户体验。用户希望应用能够快速启动并迅速响应他们的操作。如果应用启动较慢,用户可能会感到不满,并且有可能选择卸载或切换到竞争对手的应用。通过启动优化,可以提高应用的启动速度,让用户获得更好的使用体验。 竞争优势

    2024年02月14日
    浏览(41)
  • Android 10.0 pms中关于启动app时获取app的ActivityInfo信息相关源码分析

     在android10.0的系统rom定制化开发中,在对于app启动时,通过Launcher调用pms来查询app的相关ActivityInfo的相关信息,然后调用 ams来启动activity,这篇来分析pms中获取app的ActivityInfo的相关信息的相关源码分析

    2024年02月02日
    浏览(73)
  • SpringBoot源码学习4——SpringBoot内嵌Tomcat启动流程源码分析

    系列文章目录和关于我 我在初学spring的时候,很懵逼,因为整个项目中不存在main方法,让我有点摸不着头脑。那时候我知道有个东西叫tomcat是它监听了端口,解析了协议调到了我的servlet。 在我初学SpringBoot的时候,很懵逼,有main方法了,但是tomcat在哪里呢,又是如何启动起

    2024年02月04日
    浏览(47)
  • Android安卓实战项目(12)—关于身体分析,BMI计算,喝水提醒,食物卡路里计算APP【支持中英文切换】生活助手类APP(源码在文末)

    B站演示 【Android安卓实战项目(12)—生活助手类APP—关于身体分析,BMI计算,喝水提醒,食物卡路里计算APP【支持中英文切换】】 https://www.bilibili.com/video/BV1Wu4y1C76j/?share_source=copy_webvd_source=b2e9b9ed746acda34f499009647748ed 这段代码是一个Android应用程序的主要活动(Activity),它是一

    2024年02月10日
    浏览(46)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包