我的编程空间,编程开发者的网络收藏夹
学习永远不晚

[Android 13]开机动画原理分析

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

[Android 13]开机动画原理分析

Android开机动画

hongxi.zhu 2023-6-12
Lineageos_20(Android T) on Pixel 2XL

目录

一、 开机动画的启动

1.1 init.rc启动相应的进程

开机动画跑起来除了需要自身进程的启动外,还肯定以来显示系统的相关进程,即一定需要SurfaceFlinger的进程的合成和送显,所以这里需要启动SurfaceFlinger服务和bootanim服务,两者是在init.rc中启动。rc的触发阶段和引用关系如下:

  • system/core/rootdir/init.rc
...import /init.${ro.hardware}.rcimport /vendor/etc/init/hw/init.${ro.hardware}.rc...# Mount filesystems and start core system services.  //在这个阶段就可以启动系统核心的服务了,包括SF等on late-init...    # Mount fstab in init.{$device}.rc by mount_all with '--late' parameter    # to only mount entries with 'latemount'. This is needed if '--early' is    # specified in the previous mount_all command on the fs stage.    # With /system mounted and properties form /system + /factory available,    # some services can be started.    trigger late-fs...
  • device/google/wahoo/init.hardware.rc
on late-fs    # Start devices by sysfs trigger    start vendor.devstart_sh    # Start services for bootanim    start vendor.power-hal-1-3    start surfaceflinger  # 先启动surfaceflinger    start bootanim        # 然后是bootanim    start vendor.hwcomposer-2-1    start vendor.configstore-hal    start vendor.gralloc-2-0...

这里说下有时候这个文件源码里名字和设备里的名字不一样,是因为这个文件编译时会改名并拷贝到vendor/etc/init/hw/init.taimen.rc,可以从device.mk看出,我的设备Pixel 2XL taimen派生于wahoo,会引用wahoo的device.mk

  • device/google/wahoo/device.mk
  PRODUCT_COPY_FILES += \    $(LOCAL_PATH)/init.hardware.rc:$(TARGET_COPY_OUT_VENDOR)/etc/init/hw/init.$(PRODUCT_HARDWARE).rc

上面init.hardware.rc中start的surfaceflingerbootanim是在它们各自的module下声明的rc文件,当build的时候,编译系统会解析Android.bp或者Android.mk获取对应的目标的rc文件,并将rc的内容include到主rc中。

  • SurfaceFlinger服务的启动文件
    frameworks/native/services/surfaceflinger/surfaceflinger.rc
service surfaceflinger /system/bin/surfaceflinger    class core animation    user system    group graphics drmrpc readproc    capabilities SYS_NICE    onrestart restart --only-if-running zygote    task_profiles HighPerformance    socket pdx/system/vr/display/client     stream 0666 system graphics u:object_r:pdx_display_client_endpoint_socket:s0    socket pdx/system/vr/display/manager    stream 0666 system graphics u:object_r:pdx_display_manager_endpoint_socket:s0    socket pdx/system/vr/display/vsync      stream 0666 system graphics u:object_r:pdx_display_vsync_endpoint_socket:s0
  • bootanim服务的启动文件
    frameworks/base/cmds/bootanimation/bootanim.rc
service bootanim /system/bin/bootanimation    class core animation    user graphics    group graphics audio    disabled    oneshot    ioprio rt 0    task_profiles MaxPerformance

1.2 surfaceflinger服务的启动

动画播放依赖于SF进程,所以它希望先启动(无法保证两者一定能有序的启动),SF进程启动后会加载它的main方法
frameworks/native/services/surfaceflinger/main_surfaceflinger.cpp

int main(int, char**) {...    // instantiate surfaceflinger    sp<SurfaceFlinger> flinger = surfaceflinger::createSurfaceFlinger();...    // initialize before clients can connect    flinger->init(); // 这里init会去设置开机动画相关的属性    // publish surface flinger    sp<IServiceManager> sm(defaultServiceManager());    sm->addService(String16(SurfaceFlinger::getServiceName()), flinger, false,                   IServiceManager::DUMP_FLAG_PRIORITY_CRITICAL | IServiceManager::DUMP_FLAG_PROTO);    // publish gui::ISurfaceComposer, the new AIDL interface    sp<SurfaceComposerAIDL> composerAIDL = new SurfaceComposerAIDL(flinger);    sm->addService(String16("SurfaceFlingerAIDL"), composerAIDL, false,                   IServiceManager::DUMP_FLAG_PRIORITY_CRITICAL | IServiceManager::DUMP_FLAG_PROTO);    startDisplayService(); // dependency on SF getting registered above...    // run surface flinger in this thread    flinger->run();  //SurfaceFinger启动,并运行在主线程,通过消息机制循环等待任务    return 0;}

frameworks/native/services/surfaceflinger/SurfaceFlinger.cpp

// Do not call property_set on main thread which will be blocked by init// Use StartPropertySetThread instead.void SurfaceFlinger::init() {    ALOGI(  "SurfaceFlinger's main thread ready to run. "            "Initializing graphics H/W...");...    mStartPropertySetThread = getFactory().createStartPropertySetThread(presentFenceReliable);// 开启一个子现场去设置开机动画相关的属性(主线程设置属性会造成block)    if (mStartPropertySetThread->Start() != NO_ERROR) {        ALOGE("Run StartPropertySetThread failed!");    }    ALOGV("Done initializing");}

frameworks/native/services/surfaceflinger/StartPropertySetThread.cpp

bool StartPropertySetThread::threadLoop() {    // Set property service.sf.present_timestamp, consumer need check its readiness    property_set(kTimestampProperty, mTimestampPropertyValue ? "1" : "0");    // Clear BootAnimation exit flag    property_set("service.bootanim.exit", "0");  //重置开机动画退出属性    property_set("service.bootanim.progress", "0");  // 开机动画进度    // Start BootAnimation if not started    property_set("ctl.start", "bootanim");  //启动开机动画的属性,这里保险起见还是会去启动bootanim服务,但是bootanim服务有可能在前面已经被init进程拉起来了    // Exit immediately    return false;  //只执行一次,设置完就退出}

这里主要是做了两个事:

  1. 重置开机动画的退出属性,开机动画进程会循环check这个属性,如果为1就结束播放并退出,这里先初始化为0
  2. 设置开机动画启动属性,如果开机动画进程没有被前面的init进程拉起来,那么这里还会再次主动让属性服务拉起开机动画进程。

接下来就是进入启动开机动画进程的流程了。

1.3 开机动画进程的启动

frameworks/base/cmds/bootanimation/bootanimation_main.cpp

int main(){    setpriority(PRIO_PROCESS, 0, ANDROID_PRIORITY_DISPLAY);    bool noBootAnimation = bootAnimationDisabled();  //检查是否禁用了开机动画    ALOGI_IF(noBootAnimation,  "boot animation disabled");    if (!noBootAnimation) {        sp<ProcessState> proc(ProcessState::self());        ProcessState::self()->startThreadPool();        // create the boot animation object (may take up to 200ms for 2MB zip)        sp<BootAnimation> boot = new BootAnimation(audioplay::createAnimationCallbacks()); //创建动画对象,这个过程会去解析的动画文件,文件越大越耗时        waitForSurfaceFlinger();  //向servicemanager检查SF进程是否正常启动了, 死循环等待SF注册成功        boot->run("BootAnimation", PRIORITY_DISPLAY); //开始跑动画逻辑        ALOGV("Boot animation set up. Joining pool.");        IPCThreadState::self()->joinThreadPool();    }    return 0;}

主要是:

  • 检查是否禁用了开机动画,如果禁用了进程直接结束,不播放Android动画(也许厂商自己用其他方式实现)
  • 创建BootAnimation对象,加载并解析动画文件,加载时间根据文件大小有关(图片大小,数量)
  • 循环等待SurfaceFlinger服务的启动(没有SF也播放不了动画)
  • 启动动画线程(BootAnimation继承于Thread类,它本身就是一个thread),开始播放动画

1.3.1 检查是否禁用了开机动画

bool bootAnimationDisabled() {    char value[PROPERTY_VALUE_MAX];    property_get("debug.sf.nobootanimation", value, "0");    if (atoi(value) > 0) {        return true;    }    property_get("ro.boot.quiescent", value, "0");    if (atoi(value) > 0) {        // Only show the bootanimation for quiescent boots if this system property is set to enabled        if (!property_get_bool("ro.bootanim.quiescent.enabled", false)) {            return true;        }    }    return false;}

主要受三个属性控制,三个属性优先级有前后。

1.3.2 创建BootAnimation对象,预加载动画文件

BootAnimation::BootAnimation(sp<Callbacks> callbacks)        : Thread(false), mLooper(new Looper(false)), mClockEnabled(true), mTimeIsAccurate(false),        mTimeFormat12Hour(false), mTimeCheckThread(nullptr), mCallbacks(callbacks) {    mSession = new SurfaceComposerClient(); // 获取SF在开机动画进程的代理,后面会使用该对象与SF跨进程通信    std::string powerCtl = android::base::GetProperty("sys.powerctl", "");    if (powerCtl.empty()) { // 判断是开机动画还是关机动画        mShuttingDown = false;    } else {        mShuttingDown = true;    }    ALOGD("%sAnimationStartTiming start time: %" PRId64 "ms", mShuttingDown ? "Shutdown" : "Boot",            elapsedRealtime());}

上面的构造方法中仅仅中只是获取了SF的session代理对象, 真正的加载逻辑在这个对象的第一次引用回调方法中(在前面的sp指针实例化时被回调)。

void BootAnimation::onFirstRef() {    status_t err = mSession->linkToComposerDeath(this);    SLOGE_IF(err, "linkToComposerDeath failed (%s) ", strerror(-err));    if (err == NO_ERROR) {        // Load the animation content -- this can be slow (eg 200ms)        // called before waitForSurfaceFlinger() in main() to avoid wait        ALOGD("%sAnimationPreloadTiming start time: %" PRId64 "ms",                mShuttingDown ? "Shutdown" : "Boot", elapsedRealtime());        preloadAnimation();        ALOGD("%sAnimationPreloadStopTiming start time: %" PRId64 "ms",                mShuttingDown ? "Shutdown" : "Boot", elapsedRealtime());    }}

加载的逻辑主要是preloadAnimation(),这个过程主要是解析bootanimation.zip文件

bool BootAnimation::preloadAnimation() {    findBootAnimationFile();  // 检索系统的几个预设定路径下是否存在bootanimation.zip文件    if (!mZipFileName.isEmpty()) {        mAnimation = loadAnimation(mZipFileName);  // 加载动画文件        return (mAnimation != nullptr);    }    return false;}

findBootAnimationFile方法是去检索系统的几个预设定路径下是否存在bootanimation.zip文件, 如果存在赋值给mZipFileName, 几个预设定路径是:(寻找是根据系统是否加密、是否是深色主题进行选择,pixel 2xl的文件是 /product/media/下)

static const char OEM_BOOTANIMATION_FILE[] = "/oem/media/bootanimation.zip";static const char PRODUCT_BOOTANIMATION_DARK_FILE[] = "/product/media/bootanimation-dark.zip";static const char PRODUCT_BOOTANIMATION_FILE[] = "/product/media/bootanimation.zip";static const char SYSTEM_BOOTANIMATION_FILE[] = "/system/media/bootanimation.zip";static const char APEX_BOOTANIMATION_FILE[] = "/apex/com.android.bootani开始播放动画mation/etc/bootanimation.zip";static const char PRODUCT_ENCRYPTED_BOOTANIMATION_FILE[] = "/product/media/bootanimation-encrypted.zip";static const char SYSTEM_ENCRYPTED_BOOTANIMATION_FILE[] = "/system/media/bootanimation-encrypted.zip";

loadAnimation()方法是去解析bootanimation.zip文件

BootAnimation::Animation* BootAnimation::loadAnimation(const String8& fn) {    if (mLoadedFiles.indexOf(fn) >= 0) {  //mLoadedFiles是根据文件名是否加载过来防止加载动画zip包多次        SLOGE("File \"%s\" is already loaded. Cyclic ref is not allowed",            fn.string());        return nullptr;    }    ZipFileRO *zip = ZipFileRO::open(fn);  //打开zip文件    if (zip == nullptr) {        SLOGE("Failed to open animation zip \"%s\": %s",            fn.string(), strerror(errno));        return nullptr;    }    ALOGD("%s is loaded successfully", fn.string());    Animation *animation =  new Animation;    animation->fileName = fn;    animation->zip = zip;    animation->clockFont.map = nullptr;    mLoadedFiles.add(animation->fileName);  //整个zip也用一个Animation来描述    parseAnimationDesc(*animation);  //解析‘desc.txt’文件,根据这个文件创建每个part的Animation对象    if (!preloadZip(*animation)) {  //加载每一个part对应的图片,并填充到animation.part.frames        releaseAnimation(animation);        return nullptr;    }    mLoadedFiles.remove(fn);    return animation;}

解析desc.txt文件

bool BootAnimation::parseAnimationDesc(Animation& animation)  {...    // Parse the description file    for (;;) {        const char* endl = strstr(s, "\n");        if (endl == nullptr) break;        String8 line(s, endl - s);        const char* l = line.string();        int fps = 0;        int width = 0;        int height = 0;        int count = 0;        int pause = 0;        int progress = 0;        int framesToFadeCount = 0;        int colorTransitionStart = 0;        int colorTransitionEnd = 0;        char path[ANIM_ENTRY_NAME_MAX];        char color[7] = "000000"; // default to black if unspecified        char clockPos1[TEXT_POS_LEN_MAX + 1] = "";        char clockPos2[TEXT_POS_LEN_MAX + 1] = "";        char dynamicColoringPartNameBuffer[ANIM_ENTRY_NAME_MAX];        char pathType;        // start colors default to black if unspecified        char start_color_0[7] = "000000";        char start_color_1[7] = "000000";        char start_color_2[7] = "000000";        char start_color_3[7] = "000000";        int nextReadPos;        int topLineNumbers = sscanf(l, "%d %d %d %d", &width, &height, &fps, &progress);        if (topLineNumbers == 3 || topLineNumbers == 4) { //解析第一行,获取width/height/fps参数            // SLOGD("> w=%d, h=%d, fps=%d, progress=%d", width, height, fps, progress);            animation.width = width;            animation.height = height;            animation.fps = fps;            if (topLineNumbers == 4) {  //如果配置了progress,一般不会配置              animation.progressEnabled = (progress != 0);            } else {              animation.progressEnabled = false;            }        } else if (sscanf(l, "dynamic_colors %" STRTO(ANIM_PATH_MAX) "s #%6s #%6s #%6s #%6s %d %d",            dynamicColoringPartNameBuffer,加载每一个part对应的图片,并填充到animation.part.frames            start_color_0, start_color_1, start_color_2, start_color_3,            &colorTransitionStart, &colorTransitionEnd)) {  //动态颜色,我们一般不配置,所以不会走这里            animation.dynamicColoringEnabled = true;            parseColor(start_color_0, animation.startColors[0]);            parseColor(start_color_1, animation.startColors[1]);            parseColor(start_color_2, animation.startColors[2]);            parseColor(start_color_3, animation.startColors[3]);            animation.colorTransitionStart = colorTransitionStart;            animation.colorTransitionEnd = colorTransitionEnd;            dynamicColoringPartName = std::string(dynamicColoringPartNameBuffer);        } else if (sscanf(l, "%c %d %d %" STRTO(ANIM_PATH_MAX) "s%n",                          &pathType, &count, &pause, path, &nextReadPos) >= 4) {  //解析第二行开始的part部分            if (pathType == 'f') {                sscanf(l + nextReadPos, " %d #%6s %16s %16s", &framesToFadeCount, color, clockPos1,                       clockPos2);            } else {                sscanf(l + nextReadPos, " #%6s %16s %16s", color, clockPos1, clockPos2);            }            // SLOGD("> type=%c, count=%d, pause=%d, path=%s, framesToFadeCount=%d, color=%s, "            //       "clockPos1=%s, clockPos2=%s",            //       pathType, count, pause, path, framesToFadeCount, color, clockPos1, clockPos2);            Animation::Part part;             if (path == dynamicColoringPartName) {                // Part is specified to use dynamic coloring.                part.useDynamicColoring = true;                part.postDynamicColoring = false;                postDynamicColoring = true;            } else {   //我们一般只配置pathType、count、pause,其他的都是空                // Part does not use dynamic coloring.                part.useDynamicColoring = false;                part.postDynamicColoring =  postDynamicColoring;            }            part.playUntilComplete = pathType == 'c';            part.framesToFadeCount = framesToFadeCount;            part.count = count;            part.pause = pause;加载            part.path = path;            part.audioData = nullptr;  //新版本还加入了开机动画每个part可以添加对应的音频文件,但是一般不配置,且不是在这里加载            part.animation = nullptr;  //这里每个part下面一般不会再嵌套动画文件了,所以它已经是节点了            if (!parseColor(color, part.backgroundColor)) {  //color和backgroundcolor没有特殊配置使用默认的black                SLOGE("> invalid color '#%s'", color);                part.backgroundColor[0] = 0.0f;                part.backgroundColor[1] = 0.0f;                part.backgroundColor[2] = 0.0f;            }            parsePosition(clockPos1, clockPos2, &part.clockPosX, &part.clockPosY);            animation.parts.add(part);  //将part加入animation对象中,这样就能拿到他的fps/type/count/pause, 但是frames数据还没有填充        }        else if (strcmp(l, "$SYSTEM") == 0) {  //一种特殊情况,不会走这里            // SLOGD("> SYSTEM");            Animation::Part part;            part.playUntilComplete = false;            part.framesToFadeCount = 0;            part.count = 1;            part.pause = 0;            part.audioData = nullptr;            part.animation = loadAnimation(String8(SYSTEM_BOOTANIMATION_FILE));            if (part.animation != nullptr)                animation.parts.add(part);        }        s = ++endl;    }    return true;}

解析完desc.txt,已经将配置文件中每个part对应到每个实际的part文件夹,但是真正的图片,还没有去加载,接下来preloadZip加载每一个part对应的图片,并填充到animation.part.frames

bool BootAnimation::preloadZip(Animation& animation) {    // read all the data structures    const size_t pcount = animation.parts.size();    void *cookie = nullptr;    ZipFileRO* zip = animation.zip;    if (!zip->startIteration(&cookie)) {        return false;    }    ZipEntryRO entry;    char name[ANIM_ENTRY_NAME_MAX];    while ((entry = zip->nextEntry(cookie)) != nullptr) {  //遍历整个zip下的文件树,一般我们都不会嵌套多层,叶子节点就是每个part文件夹下的文件        const int foundEntryName = zip->getEntryFileName(entry, name, ANIM_ENTRY_NAME_MAX);        if (foundEntryName > ANIM_ENTRY_NAME_MAX || foundEntryName == -1) {            SLOGE("Error fetching entry file name");            continue;        }        const String8 entryName(name);        const String8 path(entryName.getPathDir());        const String8 leaf(entryName.getPathLeaf());        if (leaf.size() > 0) {...            for (size_t j = 0; j < pcount; j++) {  //遍历每个part                if (path == animation.parts[j].path) {                    uint16_t method;                    // supports only stored png files                    if (zip->getEntryInfo(entry, &method, nullptr, nullptr, nullptr, nullptr, nullptr)) {                        if (method == ZipFileRO::kCompressStored) {  //从直接可以知道bootanimation.zip必须使用存储方式打包,不能压缩,否则将不能播放FileMap* map = zip->createEntryFileMap(entry);if (map) {    Animation::Part& part(animation.parts.editItemAt(j));    if (leaf == "audio.wav") { // 如果是音频文件        // a part may have at most one audio file        part.audioData = (uint8_t *)map->getDataPtr();        part.audioLength = map->getDataLength();    } else if (leaf == "trim.txt") {  //一般不会使用trim        part.trimData.setTo((char const*)map->getDataPtr(),map->getDataLength());    } else {        Animation::Frame frame;        frame.name = leaf;        frame.map = map;        frame.trimWidth = animation.width;        frame.trimHeight = animation.height;        frame.trimX = 0;        frame.trimY = 0;        part.frames.add(frame); // 将part下面的图片添加到part的frames    }}                        } else {SLOGE("bootanimation.zip is compressed; must be only stored");                        }                    }                }            }        }    }...    zip->endIteration(cookie);    return true;}

到这里动画文件的预加载就完成了,这时候需要check SF是否已经正常启动。

1.3.3 等待SF服务的启动

frameworks/base/cmds/bootanimation/BootAnimationUtil.cpp

void waitForSurfaceFlinger() {    // TODO: replace this with better waiting logic in future, b/35253872    int64_t waitStartTime = elapsedRealtime();    sp<IServiceManager> sm = defaultServiceManager();    const String16 name("SurfaceFlinger");    const int SERVICE_WAIT_SLEEP_MS = 100;    const int LOG_PER_RETRIES = 10;    int retry = 0;    while (sm->checkService(name) == nullptr) {        retry++;        if ((retry % LOG_PER_RETRIES) == 0) {            ALOGW("Waiting for SurfaceFlinger, waited for %" PRId64 " ms",                  elapsedRealtime() - waitStartTime);        }        usleep(SERVICE_WAIT_SLEEP_MS * 1000);    };    int64_t totalWaited = elapsedRealtime() - waitStartTime;    if (totalWaited > SERVICE_WAIT_SLEEP_MS) {        ALOGI("Waiting for SurfaceFlinger took %" PRId64 " ms", totalWaited);    }}

向ServiceMananger询问SF是否注册,如果没注册死循环,每秒问一次,启动了就开始播放动画

1.3.4 启动播放线程,播放动画

前面的main方法的boot->run("BootAnimation", PRIORITY_DISPLAY);会先走BootAnimation线程的readyToRun, 然后才执行真正的线程循环体threadLoop,先看下readyToRun

  • readyToRun
status_t BootAnimation::readyToRun() {...    mDisplayToken = SurfaceComposerClient::getInternalDisplayToken();  //获取DisplayToken, 用于获取Display(屏幕)的参数    if (mDisplayToken == nullptr)        return NAME_NOT_FOUND;    DisplayMode displayMode;    const status_t error =            SurfaceComposerClient::getActiveDisplayMode(mDisplayToken, &displayMode);  //获取DisplayMode,用于获取分辨率等信息...    resolution = limitSurfaceSize(resolution.width, resolution.height);    // create the native surface    sp<SurfaceControl> control = session()->createSurface(String8("BootAnimation"),            resolution.getWidth(), resolution.getHeight(), PIXEL_FORMAT_RGB_565);  //创建Surface并返回一个surfacecontrol对象,动画需要他通过opengl绘制到surface上    SurfaceComposerClient::Transaction t;  //创建与SF通信的事务...    // Scale forced resolution to physical resolution    Rect forcedRes(0, 0, resolution.width, resolution.height);    Rect physRes(0, 0, displayMode.resolution.width, displayMode.resolution.height);    t.setDisplayProjection(mDisplayToken, ui::ROTATION_0, forcedRes, physRes);  //设置屏幕的投影参数    t.setLayer(control, 0x40000000) //将Layer和SurfaceControl绑定        .apply(); //提交事务    sp<Surface> s = control->getSurface(); //获取一个surface//初始化opengl and egl    // initialize opengl and egl    EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);    eglInitialize(display, nullptr, nullptr);    EGLConfig config = getEglConfig(display);    EGLSurface surface = eglCreateWindowSurface(display, config, s.get(), nullptr);    // Initialize egl context with client version number 2.0.    EGLint contextAttributes[] = {EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE};    EGLContext context = eglCreateContext(display, config, nullptr, contextAttributes);    EGLint w, h;    eglQuerySurface(display, surface, EGL_WIDTH, &w);    eglQuerySurface(display, surface, EGL_HEIGHT, &h);    if (eglMakeCurrent(display, surface, surface, context) == EGL_FALSE)        return NO_INIT;    mDisplay = display;    mContext = context;    mSurface = surface;    mInitWidth = mWidth = w;    mInitHeight = mHeight = h;    mFlingerSurfaceControl = control;    mFlingerSurface = s;    mTargetInset = -1;    ...    projectSceneToWindow(); // 裁剪窗口、转化坐标    // Register a display event receiver    mDisplayEventReceiver = std::make_unique<DisplayEventReceiver>();    status_t status = mDisplayEventReceiver->initCheck();    SLOGE_IF(status != NO_ERROR, "Initialization of DisplayEventReceiver failed with status: %d",            status);    mLooper->addFd(mDisplayEventReceiver->getFd(), 0, Looper::EVENT_INPUT,            new DisplayEventCallback(this), nullptr);    return NO_ERROR;}
  • threadLoop
bool BootAnimation::threadLoop() {    bool result;    initShaders();  //初始化opengl着色器    // We have no bootanimation file, so we use the stock android logo    // animation.    if (mZipFileName.isEmpty()) {        ALOGD("No animation file");        result = android();  //当我们没有定制bootanimation.zip(不存在这个文件)时,系统会去播放assets下面那两张图片,一个android字样的闪烁动画    } else {        result = movie();  //当存在bootanimation.zip时,走这个分支    }    mCallbacks->shutdown();    eglMakeCurrent(mDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);    eglDestroyContext(mDisplay, mContext);    eglDestroySurface(mDisplay, mSurface);    mFlingerSurface.clear();    mFlingerSurfaceControl.clear();    eglTerminate(mDisplay);    eglReleaseThread();    IPCThreadState::self()->stopProcess();    return result;}

我们主要看下movie的情况,这个名字也是很直白,开机动画的实现是图片逐帧动画,和电影的那种原理相同

bool BootAnimation::movie() {...    // mCallbacks->init() may get called recursively,    // this loop is needed to get the same results    for (const Animation::Part& part : mAnimation->parts) {        if (part.animation != nullptr) {            mCallbacks->init(part.animation->parts); // 初始化每一个part的音频(一般没音频)        }    }    mCallbacks->init(mAnimation->parts);...//配置Blend    // Blend required to draw time on top of animation frames.    glBlendFunc(GL_class="lazy" data-src_ALPHA, GL_ONE_MINUS_class="lazy" data-src_ALPHA);    glDisable(GL_DITHER);    glDisable(GL_SCISSOR_TEST);    glDisable(GL_BLEND);// 开启2D纹理的配置    glEnable(GL_TEXTURE_2D);    glBindTexture(GL_TEXTURE_2D, 0);    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);    bool clockFontInitialized = false;    if (mClockEnabled) {  //一般不配置显示这个时钟,不走这里        clockFontInitialized =            (initFont(&mAnimation->clockFont, CLOCK_FONT_ASSET) == NO_ERROR);        mClockEnabled = clockFontInitialized;    }    initFont(&mAnimation->progressFont, PROGRESS_FONT_ASSET);  //初始化字体。用于绘制始终和进度,一般不配置...    playAnimation(*mAnimation);  //真正的播放...    releaseAnimation(mAnimation);    mAnimation = nullptr;    return false;}

最终的播放逻辑在playAnimation

bool BootAnimation::playAnimation(const Animation& animation) {    const size_t pcount = animation.parts.size();    nsecs_t frameDuration = s2ns(1) / animation.fps;    SLOGD("%sAnimationShownTiming start time: %" PRId64 "ms", mShuttingDown ? "Shutdown" : "Boot",            elapsedRealtime());    int fadedFramesCount = 0;    int lastDisplayedProgress = 0;    int colorTransitionStart = animation.colorTransitionStart;    int colorTransitionEnd = animation.colorTransitionEnd;    for (size_t i=0 ; i<pcount ; i++) {        const Animation::Part& part(animation.parts[i]);        const size_t fcount = part.frames.size();        // Handle animation package        if (part.animation != nullptr) {  //如果存在嵌套动画文件,就递归去播放,一般没有            playAnimation(*part.animation);            if (exitPending())                break;            continue; //to next part        }        // process the part not only while the count allows but also if already fading        //如果part的count = 0 或者 还没有循环到part的count次数,就继续循环        for (int r=0 ; !part.count || r<part.count || fadedFramesCount > 0 ; r++) {            if (shouldStopPlayingPart(part, fadedFramesCount, lastDisplayedProgress)) break; //每次循环进入播放part时检查是否应该退出,判断是否boot完成且part的repeat-type不是c,即p时才能退出...            mCallbacks->playPart(i, part, r); //播放part里面的音频,一般没音频            glClearColor(                    part.backgroundColor[0],                    part.backgroundColor[1],                    part.backgroundColor[2],                    1.0f);            ALOGD("Playing files = %s/%s, Requested repeat = %d, playUntilComplete = %s",                    animation.fileName.string(), part.path.string(), part.count,                    part.playUntilComplete ? "true" : "false");            // For the last animation, if we have progress indicator from            // the system, display it.            int currentProgress = android::base::GetIntProperty(PROGRESS_PROP_NAME, 0);            bool displayProgress = animation.progressEnabled &&                (i == (pcount -1)) && currentProgress != 0;            for (size_t j=0 ; j<fcount ; j++) {                if (shouldStopPlayingPart(part, fadedFramesCount, lastDisplayedProgress)) break;  // 每次循环进入播放每一个图片时检查是否应该退出,判断是否boot完成且part的repeat-type不是c,即p时才能退出...                processDisplayEvents();  //轮询是否有display的回调事件                const double ratio_w = static_cast<double>(mWidth) / mInitWidth;                const double ratio_h = static_cast<double>(mHeight) / mInitHeight;                const int animationX = (mWidth - animation.width * ratio_w) / 2;                const int animationY = (mHeight - animation.height * ratio_h) / 2;                const Animation::Frame& frame(part.frames[j]);  //拿到具体part中的某张图片                nsecs_t lastFrame = systemTime();                if (r > 0) {                    glBindTexture(GL_TEXTURE_2D, frame.tid);                } else {  //如果是第一次播放图片(第一张图片)                    glGenTextures(1, &frame.tid);  //生成纹理                    glBindTexture(GL_TEXTURE_2D, frame.tid);  //将图片的句柄和纹理绑定                    int w, h;                    // Set decoding option to alpha unpremultiplied so that the R, G, B channels                    // of transparent pixels are preserved.                    initTexture(frame.map, &w, &h, false );  //初始化纹理                }                const int trimWidth = frame.trimWidth * ratio_w;                const int trimHeight = frame.trimHeight * ratio_h;                const int trimX = frame.trimX * ratio_w;                const int trimY = frame.trimY * ratio_h;                const int xc = animationX + trimX;                const int yc = animationY + trimY;                glClear(GL_COLOR_BUFFER_BIT);                // specify the y center as ceiling((mHeight - frame.trimHeight) / 2)                // which is equivalent to mHeight - (yc + frame.trimHeight)                const int frameDrawY = mHeight - (yc + trimHeight);                float fade = 0;...                glUseProgram(mImageShader);                glUniform1i(mImageTextureLocation, 0);                glUniform1f(mImageFadeLocation, fade);                if (animation.dynamicColoringEnabled) {                    glUniform1f(mImageColorProgressLocation, colorProgress);                }                glEnable(GL_BLEND);                drawTexturedQuad(xc, frameDrawY, trimWidth, trimHeight);                glDisable(GL_BLEND);...                handleViewport(frameDuration);  //处理视口的区域的变化                eglSwapBuffers(mDisplay, mSurface);  //交换显示buffer,显示出来                nsecs_t now = systemTime();                nsecs_t delay = frameDuration - (now - lastFrame);                //SLOGD("%lld, %lld", ns2ms(now - lastFrame), ns2ms(delay));                lastFrame = now;                if (delay > 0) {                    struct timespec spec;                    spec.tv_sec  = (now + delay) / 1000000000;                    spec.tv_nsec = (now + delay) % 1000000000;                    int err;                    do {                        err = clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &spec, nullptr);                    } while (err == EINTR);                }                checkExit();  //每一个帧显示完检查下是否需要退出,具体就是去查询boot是否完成,如果boot完成SF会设置某个系统属性,那么就会设置线程的请求退出的标志            }            usleep(part.pause * ns2us(frameDuration)); //播放完本次part了,如果pause != 0 就需要延时(pause * frame时长)的时间再接着往下播放当前part的下一个循环或者下一个part//这里的判断很重要,从前面part的for循环和退出判断可知,当part的repeat-type为c时,且count = 0时,//即使boot完成了也不会退出循环,会进行播放,但是什么时候退出呢,答案就是下面的判断,如果boot完成了(exitPending() = true)且part = 0, 即part的配置为(c 0 x)时会结束循环,//所以可知当配置了part repeat为c时,那么这个part至少会播放一次,如果一个zip里有多个c-part时,//每个c-part都至少播放一次,无论它的count是0还是非0,即使boot完成。            if (exitPending() && !part.count && mCurrentInset >= mTargetInset &&                !part.hasFadingPhase()) {                if (lastDisplayedProgress != 0 && lastDisplayedProgress != 100) {                    android::base::SetProperty(PROGRESS_PROP_NAME, "100");                    continue;                }                break; // exit the infinite non-fading part when it has been played at least once            }        }    }    // Free textures created for looping parts now that the animation is done.    for (const Animation::Part& part : animation.parts) {        if (part.count != 1) {            const size_t fcount = part.frames.size();            for (size_t j = 0; j < fcount; j++) {                const Animation::Frame& frame(part.frames[j]);                glDeleteTextures(1, &frame.tid);            }        }    }    ALOGD("%sAnimationShownTiming End time: %" PRId64 "ms", mShuttingDown ? "Shutdown" : "Boot",            elapsedRealtime());    return true;}void BootAnimation::checkExit() {    // Allow surface flinger to gracefully request shutdown  //从这里我们也可以知道是SF直接设置这个属性让开机动画退出    char value[PROPERTY_VALUE_MAX];    property_get(EXIT_PROP_NAME, value, "0"); //service.bootanim.exit = 1,则调用requestExit()    int exitnow = atoi(value);    if (exitnow) {        requestExit();  //这个是Thread类的方法,调用这个方法后exitPending() = true    }}bool BootAnimation::shouldStopPlayingPart(const Animation::Part& part,              const int fadedFramesCount,              const int lastDisplayedProgress) {    // stop playing only if it is time to exit and it's a partial part which has been faded out    return exitPending() && !part.playUntilComplete && fadedFramesCount >= part.framesToFadeCount &&        (lastDisplayedProgress == 0 || lastDisplayedProgress == 100);  //part.playUntilComplete就是part的repeat type ->c或者p}

二、结束开机动画

2.1 system server进程

开机动画的结束是开机过程中,系统完成开机完成,即将显示Home应用时调用的,具体是在performEnableScreen方法中
framework/base/services/core/java/com/android/server/wm/WindowManagerService.java

    private void performEnableScreen() {        synchronized (mGlobalLock) {            ProtoLog.i(WM_DEBUG_BOOT, "performEnableScreen: mDisplayEnabled=%b"+ " mForceDisplayEnabled=%b" + " mShowingBootMessages=%b"+ " mSystemBooted=%b mOnlyCore=%b. %s", mDisplayEnabled,                    mForceDisplayEnabled, mShowingBootMessages, mSystemBooted, mOnlyCore,                    new RuntimeException("here").fillInStackTrace());    ...                if (!mBootAnimationStopped) {                Trace.asyncTraceBegin(TRACE_TAG_WINDOW_MANAGER, "Stop bootanim", 0);                // stop boot animation                // formerly we would just kill the process, but we now ask it to exit so it                // can choose where to stop the animation.                SystemProperties.set("service.bootanim.exit", "1");  //设置开机动画退出属性,开机动画播放线程循环中会去check这个属性是否设置为1,然后请求退出循环                mBootAnimationStopped = true;            }            if (!mForceDisplayEnabled && !checkBootAnimationCompleteLocked()) {                ProtoLog.i(WM_DEBUG_BOOT, "performEnableScreen: Waiting for anim complete");                return;            }            try {            //跨进程通知SF去处理让开机动画退出                IBinder surfaceFlinger = ServiceManager.getService("SurfaceFlinger");                if (surfaceFlinger != null) {                    ProtoLog.i(WM_ERROR, "******* TELLING SURFACE FLINGER WE ARE BOOTED!");                    Parcel data = Parcel.obtain();                    data.writeInterfaceToken("android.ui.ISurfaceComposer");                    surfaceFlinger.transact(IBinder.FIRST_CALL_TRANSACTION, // BOOT_FINISHEDdata, null, 0);  //注意这个IBinder.FIRST_CALL_TRANSACTION,这是system server第一次和SF binder通信,SF当收到是这个FLAG时处理通知关闭开机动画                    data.recycle();                }            } catch (RemoteException ex) {                ProtoLog.e(WM_ERROR, "Boot completed: SurfaceFlinger is dead!");            }            EventLogTags.writeWmBootAnimationDone(SystemClock.uptimeMillis());            Trace.asyncTraceEnd(TRACE_TAG_WINDOW_MANAGER, "Stop bootanim", 0);            mDisplayEnabled = true;            ProtoLog.i(WM_DEBUG_SCREEN_ON, "******************** ENABLING SCREEN!");            // Enable input dispatch.            mInputManagerCallback.setEventDispatchingLw(mEventDispatchingEnabled);  //开始使能input dispatch 可以开始分发事件        }        try {            mActivityManager.bootAnimationComplete(); // 告知AMS开机动画已经完成        } catch (RemoteException e) {        }        mPolicy.enableScreenAfterBoot();        // Make sure the last requested orientation has been applied.        updateRotationUnchecked(false, false);    }

2.2 SurfaceFlinger进程

WMS跨进程调用到SurfaceFlinger::onTransact中处理
frameworks/native/services/surfaceflinger/SurfaceFlinger.cpp

status_t SurfaceFlinger::onTransact(uint32_t code, const Parcel& data, Parcel* reply,        uint32_t flags) {    if (const status_t error = CheckTransactCodeCredentials(code); error != OK) {        return error;    }    status_t err = BnSurfaceComposer::onTransact(code, data, reply, flags);  //由SurfaceFlinger父类处理    ...}

frameworks/native/libs/gui/include/gui/ISurfaceComposer.h

class BnSurfaceComposer: public BnInterface<ISurfaceComposer> {public:    enum ISurfaceComposerTag {        // Note: BOOT_FINISHED must remain this value, it is called from        // Java by ActivityManagerService.        BOOT_FINISHED = IBinder::FIRST_CALL_TRANSACTION,  //远程调用的FLAG, 实际上是BOOT_FINISHED...    };    virtual status_t onTransact(uint32_t code, const Parcel& data,            Parcel* reply, uint32_t flags = 0);};

frameworks/native/libs/gui/ISurfaceComposer.cpp

status_t BnSurfaceComposer::onTransact(  //由上面可知,在父类中处理BOOT_FINISHED    uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags){    switch(code) {...        case BOOT_FINISHED: {            CHECK_INTERFACE(ISurfaceComposer, data, reply);            bootFinished();  //父类中的纯虚函数需要子类实现,所以会调用SF子类的实现            return NO_ERROR;        }

frameworks/native/services/surfaceflinger/SurfaceFlinger.cpp
父类中的纯虚函数需要子类实现,所以会调用SF的实现

void SurfaceFlinger::bootFinished() {...    // stop boot animation    // formerly we would just kill the process, but we now ask it to exit so it    // can choose where to stop the animation.    property_set("service.bootanim.exit", "1");  //设置service.bootanim.exit -> 1 告知开机动画进程退出    const int LOGTAG_SF_STOP_BOOTANIM = 60110;    LOG_EVENT_LONG(LOGTAG_SF_STOP_BOOTANIM,                   ns2ms(systemTime(SYSTEM_TIME_MONOTONIC)));    sp<IBinder> input(defaultServiceManager()->getService(String16("inputflinger")));...}

到这里退出开机动画的时间点和动作就了解清楚了。开机动画基本已经分析结束。

来源地址:https://blog.csdn.net/qq_40731414/article/details/131170160

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

[Android 13]开机动画原理分析

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

Android Handler 机制实现原理分析

handler在安卓开发中是必须掌握的技术,但是很多人都是停留在使用阶段。使用起来很简单,就两个步骤,在主线程重写handler的handleMessage( )方法,在工作线程发送消息。但是,有没有人想过这种技术是怎么实现的呢?下面我们一
2022-06-06

Android系统的开机画面显示过程分析

函数fb_find_logo实现在文件kernel/goldfish/drivers/video/logo/logo.c文件中,如下所示:extern const struct linux_logo logo_linux_mono;  ex
2023-01-31

Android消息机制原理深入分析

这篇文章主要介绍了Android消息机制原理,Android的消息机制主要是指Handler的运行机制以及Handler所附带的MessageQueue和Looper的工作过程
2022-12-09

基于chatgpt开发QQ机器人原理分析

ChatGPT是当前自然语言处理领域的重要进展之一,可应用于多种场景,如智能客服、聊天机器人、语音助手等。本文通过调用OpenAIGPT-3模型提供的CompletionAPI来实现一个更加智能的QQ机器人,文中原理代码介绍的非常详细,感兴趣的同学可以参考下
2023-05-18

Android开发组件化架构设计原理实例分析

今天小编给大家分享一下Android开发组件化架构设计原理实例分析的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。为什么需要组
2023-07-02

android开机自启动原理与实现案例(附源码)

原理: Android系统通过应用程序自行在系统中登记注册事件(即Intent)来响应系统产生的各类消息。 Android系统为应用程序管理功能提供了大量的API,通过配置Intent和permission来实现各种功能。 开机自启动是通过
2022-06-06

Android事件分发机制深入刨析原理及源码

Android 的事件分发机制大体可以分为三部分:事件生产、事件分发 、事件消费。事件的生产是由用户点击屏幕产生,我们这次着重分析事件的分发和消费,因为事件分发和处理联系的过于紧密,这篇文章将把事件的分发和消费放在一起分析
2023-05-16

java编程之AC自动机工作原理的示例分析

这篇文章将为大家详细讲解有关java编程之AC自动机工作原理的示例分析,小编觉得挺实用的,因此分享给大家做个参考,希望大家阅读完这篇文章后可以有所收获。1.应用场景—多模字符串匹配我们现在考虑这样一个问题,在一个文本串text中,我们想找出
2023-05-30

java开发分布式服务框架Dubbo原理机制的示例分析

这篇文章给大家分享的是有关java开发分布式服务框架Dubbo原理机制的示例分析的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。前言在介绍Dubbo之前先了解一下基本概念:Dubbo是一个RPC框架,RPC,即Re
2023-06-25

python语言开发垃圾回收机制原理的示例分析

这篇文章主要介绍python语言开发垃圾回收机制原理的示例分析,文中介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们一定要看完!一.什么是垃圾回收机制垃圾回收机制(简称GC), 解释器自带的一种机制它是一种动态存储管理技术,自动释放不再
2023-06-25

win7系统开机启动项不能加载的原因分析及解决

开机启动项是每台电脑都有的东西,就是多和少的问题的,很多人开机的时候喜欢加载很多的启动项,其实这也没什么不好的。现在的电脑为了受到更好的保护,往往在开机的时QeWZq候就加载了一些启动项,如:杀毒软件,安http://www.cppcns.
2023-05-29

Java注解机制之Spring自动装配实现原理的示例分析

小编给大家分享一下Java注解机制之Spring自动装配实现原理的示例分析,希望大家阅读完这篇文章之后都有所收获,下面让我们一起去探讨吧! Java中使用注解的情况主要在SpringMVC(Spring Boot等),注解实际上相当于一种标
2023-05-31

mybatis-plus雪花算法自动生成机器id原理的示例分析

这篇文章主要介绍了mybatis-plus雪花算法自动生成机器id原理的示例分析,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。1、雪花算法原理 雪花算法使用一个
2023-06-15

Android源码面试宝典之JobScheduler从使用到原理分析(二)【JSS的启动】

上文,我们以IntentService入手,先对JobScheduler进行了简单的实例编码使用。本文开始,我们开始就源码入手,开始深入学习、总结JobScheduler的内部实现原理。 前言 我们从使用代码入手,通过阅读JobSch
2023-08-16

win7开机提示系统自动修复无法正常进入的原因分析及解决

故障现象:近期使用Windows 7操作系统开机提示自动修复,无法正常进入操作系统。原因分析:经过分析,部分系统修复报错文件为:X:Windowssystem32driveRSSpwww.cppcns.comoon.sys如下图:解决方案:
2023-05-29

编程热搜

  • Android:VolumeShaper
    VolumeShaper(支持版本改一下,minsdkversion:26,android8.0(api26)进一步学习对声音的编辑,可以让音频的声音有变化的播放 VolumeShaper.Configuration的三个参数 durati
    Android:VolumeShaper
  • Android崩溃异常捕获方法
    开发中最让人头疼的是应用突然爆炸,然后跳回到桌面。而且我们常常不知道这种状况会何时出现,在应用调试阶段还好,还可以通过调试工具的日志查看错误出现在哪里。但平时使用的时候给你闹崩溃,那你就欲哭无泪了。 那么今天主要讲一下如何去捕捉系统出现的U
    Android崩溃异常捕获方法
  • android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
    系统的设置–>电池–>使用情况中,统计的能耗的使用情况也是以power_profile.xml的value作为基础参数的1、我的手机中power_profile.xml的内容: HTC t328w代码如下:
    android开发教程之获取power_profile.xml文件的方法(android运行时能耗值)
  • Android SQLite数据库基本操作方法
    程序的最主要的功能在于对数据进行操作,通过对数据进行操作来实现某个功能。而数据库就是很重要的一个方面的,Android中内置了小巧轻便,功能却很强的一个数据库–SQLite数据库。那么就来看一下在Android程序中怎么去操作SQLite数
    Android SQLite数据库基本操作方法
  • ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
    工作的时候为了方便直接打开编辑文件,一些常用的软件或者文件我们会放在桌面,但是在ubuntu20.04下直接直接拖拽文件到桌面根本没有效果,在进入桌面后发现软件列表中的软件只能收藏到面板,无法复制到桌面使用,不知道为什么会这样,似乎并不是很
    ubuntu21.04怎么创建桌面快捷图标?ubuntu软件放到桌面的技巧
  • android获取当前手机号示例程序
    代码如下: public String getLocalNumber() { TelephonyManager tManager =
    android获取当前手机号示例程序
  • Android音视频开发(三)TextureView
    简介 TextureView与SurfaceView类似,可用于显示视频或OpenGL场景。 与SurfaceView的区别 SurfaceView不能使用变换和缩放等操作,不能叠加(Overlay)两个SurfaceView。 Textu
    Android音视频开发(三)TextureView
  • android获取屏幕高度和宽度的实现方法
    本文实例讲述了android获取屏幕高度和宽度的实现方法。分享给大家供大家参考。具体分析如下: 我们需要获取Android手机或Pad的屏幕的物理尺寸,以便于界面的设计或是其他功能的实现。下面就介绍讲一讲如何获取屏幕的物理尺寸 下面的代码即
    android获取屏幕高度和宽度的实现方法
  • Android自定义popupwindow实例代码
    先来看看效果图:一、布局
  • Android第一次实验
    一、实验原理 1.1实验目标 编程实现用户名与密码的存储与调用。 1.2实验要求 设计用户登录界面、登录成功界面、用户注册界面,用户注册时,将其用户名、密码保存到SharedPreference中,登录时输入用户名、密码,读取SharedP
    Android第一次实验

目录