/ 今日科技快讯 /
据报道,多位知情人士透露,苹果公司正与腾讯和字节跳动商谈将二者的人工智能模型整合到在中国销售的iPhone中。苹果本月开始在其设备中接入OpenAI的ChatGPT,Siri会调用ChatGPT,不过ChatGPT并未在中国上市。据悉,苹果与腾讯和字节就使用二者人工智能模型的讨论还处于非常早期的阶段。
/ 作者简介 /
明天周六啦,提前祝大家周末愉快!
本篇文章转自FerdinandHu的博客,文章主要分享了自定义开机动画,相信会对大家有所帮助!
原文地址:
https://juejin.cn/post/7449284933204000802
/ 前言 /
本篇博客将全面记录对 Android 开机动画机制的学习过程,最终实现自定义开机动画,真正达成“从源码到定制”的目标。本博客的源码分析基于 Android 13_r16 版本展开。
/ 分析思路记录 /
假如现在接到领导的需求:“Ferdinand,你把这个安卓系统的开机动画改一下,不要显示Android,要显示咱们公司的LOGO。”我当时心里一惊:“啊?我从没做过这个啊!领导,您看,能不能让我先学学这个开机动画怎么改?”别着急,先别慌,遇事找解决方案,先把问题抛给ChatGPT,看看它怎么帮我这个‘技术小白’脱困!
问ChatGPT:帮我分析开机android13的开机动画源码并自定义开机动画。ChatGPT答:
巴拉巴拉......根据ChatGPT的回答,首先找到开机动画的源码位置,看看有哪些内容。
从上面的截图中可以看到,开机动画模块并不是特别复杂,主要由bootanimation_main、BootAnimation、BootAnimationUtil和audioplay这几个部分组成。从这些文件的名称中也大致可以看出其功能,bootanimation_main负责启动整个开机动画模块,BootAnimation应该是具体实现动画显示部分,BootAnimationUtil是一些工具辅助类,audioplay则应该是负责开机动画的声音部分。这些源码部分咱们暂且搁置,先分析每个模块的必要部分Android.bp文件和bootanim.rc文件。看看从这两个文件中能获取一些什么信息。
Android.bp 文件分析
Android.bp文件如下所示,具体的解释分析也包含在内:
package {
// See: http://go/android-license-faq
// A large-scale-change added 'default_applicable_licenses' to import
// all of the 'license_kinds' from "frameworks_base_license"
// to get the below license kinds:
// SPDX-license-identifier-Apache-2.0
// 定义了包的许可证信息
default_applicable_licenses: ["frameworks_base_license"],
}
//cc_defaults定义了一些通用的编译选项和依赖,它本身不会生成任何目标文件
cc_defaults {
name: "bootanimation_defaults",
// 编译时的标志选项,这里启用了OpenGL扩展,以及开启多种编译警告。
cflags: [
"-DGL_GLEXT_PROTOTYPES",
"-DEGL_EGLEXT_PROTOTYPES",
"-Wall", // 开启所有警告
"-Werror", // 将警告视为错误
"-Wunused", // 检测未使用的变量
"-Wunreachable-code", // 检测不可达代码
],
// 列出了共享库的依赖项
shared_libs: [
"libandroidfw",
"libbase",
"libbinder",
"libcutils",
"liblog",
"libutils",
],
}
// bootanimation executable
// =========================================================
// 定义了一个名为bootanimation的可执行文件模块,最终会被编译到/system/bin/bootanimation
cc_binary {
name: "bootanimation",
defaults: ["bootanimation_defaults"], // 引用了上面定义的bootanimation_defaults模块
// 头文件引用了jni_headers模块,是 Android 平台提供的一个通用模块,它被定义在 Android 的公共代码库中。
header_libs: ["jni_headers"],
// 列出该可执行文件所依赖的共享库
shared_libs: [
"libOpenSLES", // 用于音频播放
"libbootanimation", // 是一个自定义库
],
// 列出了需要编译的源文件
srcs: [
"BootAnimationUtil.cpp", // 包含开机动画播放相关的工具函数
"bootanimation_main.cpp", // 主程序入口
"audioplay.cpp", // 音频播放相关的实现文件
],
// 指定与该可执行文件相关的init.rc文件,最终被拷贝到/system/etc/init/bootanim.rc
init_rc: ["bootanim.rc"],
// 编译选项:禁止显示废弃声明的警告
cflags: [
"-Wno-deprecated-declarations",
],
}
// libbootanimation
// ===========================================================
// 定义了一个共享库libbootanimation,最终被拷贝到/system/lib64/libbootanimation.so
cc_library_shared {
name: "libbootanimation",
defaults: ["bootanimation_defaults"],
// 列出了源文件,BootAnimation.cpp被编译成库文件。
srcs: ["BootAnimation.cpp"],
shared_libs: [
"libui",
"libjnigraphics",
"libEGL",
"libGLESv2",
"libgui",
],
}
从Android.bp文件中我们可以了解到,开机动画模块哪些内容被编译成库,哪些内容被编译成可执行文件,这让我们对这个模块有了一个初步的了解,接下来继续分析bootanima.rc文件。
bootanim.rc文件分析
# 定义了一个服务bootanim,它对应的可执行文件为/system/bin/bootanimation,和我们上面分析Android.bp文件一致
service bootanim /system/bin/bootanimation
class core animation # 该服务属于core和animation类,core类表示它是Android系统的核心组成部分
user graphics # 该服务属于graphics用户
group graphics audio # 该服务属于graphics和audio用户组
disabled # 表明该服务不会自己启动,需要其他进程拉起
oneshot # 表明该服务只运行一次
ioprio rt 0 # 设置io优先级为real-time
task_profiles MaxPerformance # 该服务使用最高性能表现
从bootanim.rc文件中我们了解到bootanim是一个开机动画服务,它不会自行启动,而需要其他进程(ChatGPT告诉我们是SurfaceFlinger)拉起它。
bootanimation_main.cpp文件分析
int main()
{
// 设置当前进程优先级为ANDROID_PRIORITY_DISPLAY = -4
setpriority(PRIO_PROCESS, 0, ANDROID_PRIORITY_DISPLAY);
// 调用 bootAnimationDisabled() 检查设备是否配置为禁用开机动画
// 内部是通过获取属性(debug.sf.nobootanimation或ro.boot.quiescent)来完成的
bool noBootAnimation = bootAnimationDisabled();
ALOGI_IF(noBootAnimation, "boot animation disabled");
// 如果有开机动画
if (!noBootAnimation) {
// 创建一个 Binder IPC 的进程状态
// 启动线程池,允许当前进程处理来自其他服务或系统组件的 IPC 请求
sp proc(ProcessState::self());
ProcessState::self()->startThreadPool();
// create the boot animation object (may take up to 200ms for 2MB zip)
// 创建一个 BootAnimation 实例
sp boot = new BootAnimation(audioplay::createAnimationCallbacks());
// 等待 SurfaceFlinger 服务启动完成
waitForSurfaceFlinger();
// 调用 BootAnimation 对象的 run 方法,开始播放开机动画
boot->run("BootAnimation", PRIORITY_DISPLAY);
ALOGV("Boot animation set up. Joining pool.");
// 将当前线程加入到 Binder IPC 线程池,保持进程持续运行以处理 IPC 请求
IPCThreadState::self()->joinThreadPool();
}
return 0;
}
从bootanimation.cpp的代码分析中可以知道,开机播放动画需要SurfaceFlinger服务启动后才开始。
BootAnimationUtil工具分析
BootAnimationUtil里面就只有三个函数:bootAnimationDisabled用于判断开机动画是否打开,waitForSurfaceFlinger用于等待直到SurfaceFlinger服务起来,playSoundsAllowed则是用于判断是否播放开机声音。接下来具体看看这三个函数的代码。
bool bootAnimationDisabled() {
// 查询系统属性 debug.sf.nobootanimation 的值,并存储到变量 value 中
// 如果value的值大于0,表示禁用开机动画
char value[PROPERTY_VALUE_MAX];
property_get("debug.sf.nobootanimation", value, "0");
if (atoi(value) > 0) {
return true;
}
// 查询系统属性 ro.boot.quiescent 的值
// 如果该值大于 0,表示系统启动处于“静默模式”(Quiescent Boot Mode)
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
// 在静默模式下,只有当 ro.bootanim.quiescent.enabled 属性为 true 时才显示开机动画,否则禁用
// 静默模式是一种启动模式,通常用于节省电量或特殊用途(如工厂模式或测试环境)
if (!property_get_bool("ro.bootanim.quiescent.enabled", false)) {
return true;
}
}
return false;
}
从这个函数的分析中,我们可以知道两种关闭开机动画的方法:一是设置 debug.sf.nobootanimation 属性为 1;二是启用静默模式下且 ro.bootanim.quiescent.enabled 属性为true。
void waitForSurfaceFlinger() {
// TODO: replace this with better waiting logic in future, b/35253872
// 记录从设备启动到当前时间的毫秒数
int64_t waitStartTime = elapsedRealtime();
sp sm = defaultServiceManager();
const String16 name("SurfaceFlinger");
const int SERVICE_WAIT_SLEEP_MS = 100;
const int LOG_PER_RETRIES = 10;
int retry = 0;
// 通过查询SurfaceFlinger服务是否注册来等待
while (sm->checkService(name) == nullptr) {
retry++;
if ((retry % LOG_PER_RETRIES) == 0) {
ALOGW("Waiting for SurfaceFlinger, waited for %" PRId64 " ms",
elapsedRealtime() - waitStartTime);
}
// 每隔100ms查询一次
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);
}
}
这部分代码存在两个潜在的问题:
一是当前采用固定时间间隔(100ms)的轮询方式,效率较低,容易浪费CPU资源,可以使用事件触发机制(如Binder回调)来减少轮询开销;
二是代码中没有设置最大等待时间,如果SurfaceFlinger启动失败,程序会陷入无限等待。后续可以通过分析开机动画等待SurfaceFlinger服务的时间,来判断是否需要做进一步的优化。
BootAnimation类代码分析
首先看看BootAnimation类定义代码BootAnimation.h(由于代码过长,省略部分)。
class BootAnimation : public Thread, public IBinder::DeathRecipient
{
public:
static constexpr int MAX_FADED_FRAMES_COUNT = std::numeric_limits<int>::max();
struct Texture {
GLint w;
GLint h;
GLuint name;
};
struct Font {
FileMap* map = nullptr;
Texture texture;
int char_width;
int char_height;
};
struct Animation {
struct Frame {
...
};
struct Part {
...
};
};
// All callbacks will be called from this class's internal thread.
class Callbacks : public RefBase {
public:
virtual void init(const Vector<:part>& /*parts*/) {}
virtual void playPart(int /*partNumber*/, const Animation::Part& /*part*/,
int /*playNumber*/) {}
virtual void shutdown() {}
};
explicit BootAnimation(sp callbacks);
virtual ~BootAnimation();
sp session() const;
private:
virtual bool threadLoop();
virtual status_t readyToRun();
virtual void onFirstRef();
virtual void binderDied(const wp& who);
bool updateIsTimeAccurate();
class TimeCheckThread : public Thread {
public:
explicit TimeCheckThread(BootAnimation* bootAnimation);
virtual ~TimeCheckThread();
private:
virtual status_t readyToRun();
virtual bool threadLoop();
bool doThreadLoop();
void addTimeDirWatch();
int mInotifyFd;
int mBootAnimWd;
int mTimeWd;
BootAnimation* mBootAnimation;
};
...
Animation* loadAnimation(const String8&);
bool playAnimation(const Animation&);
void releaseAnimation(Animation*) const;
bool parseAnimationDesc(Animation&);
bool preloadZip(Animation &animation);
void findBootAnimationFile();
bool findBootAnimationFileInternal(const std::vector<std::string>& files);
bool preloadAnimation();
...
};
从BootAnimation类定义的内容来看,它是同时继承Thread和IBinder::DeathRecipient。从bootanimation.cpp代码中看到,播放动画是通过BootAnimation类的run函数,实际上这里并没有重写run函数(Qt中的QThread类一般是重写run()函数),而是重写threadLoop()函数(run()内部自动调用threadLoop())。这一部分体现了Qt与Android的线程设计的不同。
因此下面将重点分析几个比较重要的开机动画实现函数:
bool BootAnimation::threadLoop() {
bool result;
initShaders(); // 通过 initShaders 准备好 OpenGL 的渲染环境
// We have no bootanimation file, so we use the stock android logo
// animation.
// 如果 bootanimation.zip 存在,则通过 movie() 播放动画
// 如果动画文件缺失,则显示默认 Android 标志,通过 android() 实现
if (mZipFileName.isEmpty()) {
ALOGD("No animation file");
result = android();
} else {
result = movie();
}
// 调用shutdown函数通知开机动画已经完成
mCallbacks->shutdown();
// 释放OpenGL和SurfaceFlinger相关资源
eglMakeCurrent(mDisplay, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
eglDestroyContext(mDisplay, mContext);
eglDestroySurface(mDisplay, mSurface);
mFlingerSurface.clear();
mFlingerSurfaceControl.clear();
eglTerminate(mDisplay);
eglReleaseThread();
// 停止IPC通信
IPCThreadState::self()->stopProcess();
return result;
}
从threadLoop()函数中可以看到整个开机动画播放的主要逻辑和重要信息:一是在Android的开机动画中,图像的绘制通过OpenGL渲染,如果想要效果更好的开机动画,应该深入OpenGL渲染这一层去实现;二是Android的开机动画应该是采用的默认开机动画,想要自定义开机动画应该重点关注movie()函数和mZipFileName相关部分。
在movie()函数内部,第一个调用的函数是loadAnimation,顾名思义它就是用来加载动画的,我们首先来分析一下这个函数。
BootAnimation::Animation* BootAnimation::loadAnimation(const String8& fn) {
// 检查是否已经加载过该路径的动画文件
if (mLoadedFiles.indexOf(fn) >= 0) {
SLOGE("File \"%s\" is already loaded. Cyclic ref is not allowed",
fn.string());
return nullptr;
}
// 打开zip文件
ZipFileRO *zip = ZipFileRO::open(fn);
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 *animation = new Animation;
animation->fileName = fn;
animation->zip = zip;
animation->clockFont.map = nullptr;
mLoadedFiles.add(animation->fileName);
// 调用 parseAnimationDesc 函数解析动画的描述文件(通常是 desc.txt)
// 从描述文件中读取动画的基本信息,例如帧率、分辨率、循环方式等
parseAnimationDesc(*animation);
// 调用 preloadZip 函数预加载 Zip 文件中的资源
if (!preloadZip(*animation)) {
releaseAnimation(animation);
return nullptr;
}
mLoadedFiles.remove(fn);
return animation;
}
通过对loadAnimation函数的分析,我们可以了解到一个重要信息就是:它是通过加载一个zip文件来实现加载动画的,它的动画资源全部包含在这个zip文件里面。我们想要自定义动画得自定义这个zip文件。
下面分析一下movie函数:
bool BootAnimation::movie() {
if (mAnimation == nullptr) {
mAnimation = loadAnimation(mZipFileName);
}
if (mAnimation == nullptr)
return false;
// 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);
}
}
mCallbacks->init(mAnimation->parts);
// 根据动画内容和系统设置决定是否启用时钟显示
bool anyPartHasClock = false;
for (size_t i=0; i parts.size(); i++) {
if(validClock(mAnimation->parts[i])) {
anyPartHasClock = true;
break;
}
}
if (!anyPartHasClock) {
mClockEnabled = false;
} else if (!android::base::GetBoolProperty(CLOCK_ENABLED_PROP_NAME, false)) {
mClockEnabled = false;
}
// Check if npot textures are supported
// 过 OpenGL 扩展检查当前设备是否支持非2次幂纹理(NPOT)。若支持,则设置 mUseNpotTextures 为 true
mUseNpotTextures = false;
String8 gl_extensions;
const char* exts = reinterpret_cast<const char*>(glGetString(GL_EXTENSIONS));
if (!exts) {
glGetError();
} else {
gl_extensions.setTo(exts);
if ((gl_extensions.find("GL_ARB_texture_non_power_of_two") != -1) ||
(gl_extensions.find("GL_OES_texture_npot") != -1)) {
mUseNpotTextures = true;
}
}
// Blend required to draw time on top of animation frames.
// 设置OpenGL
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glDisable(GL_DITHER);
glDisable(GL_SCISSOR_TEST);
glDisable(GL_BLEND);
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);
// 如果启用了时钟功能(mClockEnabled 为 true),则初始化时钟字体
bool clockFontInitialized = false;
if (mClockEnabled) {
clockFontInitialized =
(initFont(&mAnimation->clockFont, CLOCK_FONT_ASSET) == NO_ERROR);
mClockEnabled = clockFontInitialized;
}
// 初始化进度字体
initFont(&mAnimation->progressFont, PROGRESS_FONT_ASSET);
// 确保开机动画中的时钟显示准确
if (mClockEnabled && !updateIsTimeAccurate()) {
mTimeCheckThread = new TimeCheckThread(this);
mTimeCheckThread->run("BootAnimation::TimeCheckThread", PRIORITY_NORMAL);
}
// 如果启用了动态着色(dynamicColoringEnabled),则初始化动态颜色
if (mAnimation->dynamicColoringEnabled) {
initDynamicColors();
}
// 播放动画
playAnimation(*mAnimation);
// 下面是一些清理工作
if (mTimeCheckThread != nullptr) {
mTimeCheckThread->requestExit();
mTimeCheckThread = nullptr;
}
if (clockFontInitialized) {
glDeleteTextures(1, &mAnimation->clockFont.texture.name);
}
releaseAnimation(mAnimation);
mAnimation = nullptr;
return false;
}
movie()函数描述了播放开机动画的整个流程,具体细节这里暂时不去研究。咱们还是得紧扣本博客的主题“如何自定义开机动画”。前面提到了mZipFileName变量里面存放的是动画路径的位置,我们看看哪个函数对这个变量进行了赋值。
根据ASFP的Find Usages找到了findBootAnimationFileInternal函数,继续寻找调用findBootAnimationFileInternal这个函数的位置,发现是findBootAnimationFileInternal()这个函数。详细分析一下这个函数的代码,应该可以找到答案。
void BootAnimation::findBootAnimationFile() {
// If the device has encryption turned on or is in process
// of being encrypted we show the encrypted boot animation.
char decrypt[PROPERTY_VALUE_MAX];
property_get("vold.decrypt", decrypt, "");
bool encryptedAnimation = atoi(decrypt) != 0 ||
!strcmp("trigger_restart_min_framework", decrypt);
// 如果设备启用了加密或正在加密过程中,系统会选择加密的启动动画文件
if (!mShuttingDown && encryptedAnimation) {
static const std::vector<std::string> encryptedBootFiles = {
PRODUCT_ENCRYPTED_BOOTANIMATION_FILE, SYSTEM_ENCRYPTED_BOOTANIMATION_FILE,
};
if (findBootAnimationFileInternal(encryptedBootFiles)) {
return;
}
}
// 系统检查系统属性 ro.boot.theme,如果值为 1,则启用暗色主题的启动动画文件
const bool playDarkAnim = android::base::GetIntProperty("ro.boot.theme", 0) == 1;
static const std::vector<std::string> bootFiles = {
APEX_BOOTANIMATION_FILE, playDarkAnim ? PRODUCT_BOOTANIMATION_DARK_FILE : PRODUCT_BOOTANIMATION_FILE,
OEM_BOOTANIMATION_FILE, SYSTEM_BOOTANIMATION_FILE
};
static const std::vector<std::string> shutdownFiles = {
PRODUCT_SHUTDOWNANIMATION_FILE, OEM_SHUTDOWNANIMATION_FILE, SYSTEM_SHUTDOWNANIMATION_FILE, ""
};
static const std::vector<std::string> userspaceRebootFiles = {
PRODUCT_USERSPACE_REBOOT_ANIMATION_FILE, OEM_USERSPACE_REBOOT_ANIMATION_FILE,
SYSTEM_USERSPACE_REBOOT_ANIMATION_FILE,
};
// 如果正在执行用户空间重启,则加载用户空间重启动画文件
// 如果是关机,则加载关机动画文件
if (android::base::GetBoolProperty("sys.init.userspace_reboot.in_progress", false)) {
findBootAnimationFileInternal(userspaceRebootFiles);
} else if (mShuttingDown) {
findBootAnimationFileInternal(shutdownFiles);
} else {
findBootAnimationFileInternal(bootFiles);
}
}
从上面的代码分析中可以知道,如果想要自定义开机动画,应该找到PRODUCT_BOOTANIMATION_FILE、OEM_BOOTANIMATION_FILE和SYSTEM_BOOTANIMATION_FILE这几个位置,它们对应的值分别是"/product/media/bootanimation.zip"、"/oem/media/bootanimation.zip"和"/system/media/bootanimation.zip"。
而且我们还可以在这里做一个定制化:那就是增加一个属性值,如果该属性值为true,则采用我们自定义的开机动画,否则依然采用源代码的动画加载方式。
/ 自定义动画 /
通过前面的分析,我们已经知道了导入动画的关键步骤了,那就是制作一个动画并把它打包成bootanimation.zip,然后push到Android的/product/media目录下面。接下来将详细介绍一个制作动画的方法。
使用FlixClip制作gif图
FlexClip是一个功能强大的在线视频编辑平台,它提供简单易用的界面和丰富的视频编辑工具,让用户能够快速创建专业的视频内容(主页在这FlixClip(https://www.flexclip.com/cn/editor/app?id=37d12fa800142293b3f84d874da40407))。如何使用这个软件我就不多赘述了,我们可以使用它的免费导出功能得到一张我们想要的gif动图。下面是我制作的LOGO动画。
gif转成序列帧
接下来将得到的gif图转成序列帧(一张一张的png图片),这里也推荐一个网站工具(链接在这BEJSON)。咱们把得到的序列帧重新命名,命名格式为“00001.png,00002.png,00003.png...”。并将它放置在新建的part0文件夹下,参考格式如下图。
接着就是编写desc.txt文件了,它是用来描述动画播放方式的。我的desc.txt的格式如下:
它表示动画的分辨率为1440 * 2556,并且以15fps的速率进行播放。l 表示循环播放, 0 0 表示无限次循环,part0表示播放part0文件夹。接着将这两个文件打包成zip文件,使用指令zip -r0 bootanimation.zip desc.txt part0。最后得到的文件如下图所示:
push bootanimation.zip
最后一步就是将bootanimation.zip文件push到emulator模拟器中。依次使用下面的命令来完成这一步骤:
打开模拟器
emulator -writable-system
另开一个终端重新挂载分区
adb root
adb remount
adb push bootanimation.zip /product/media
最后重启
adb reboot
这里不仅可以push到product分区,push到/system/media一样也是可以的,通过第六节的分析我们可以知道。
/ 效果与总结 /
效果如下:
本文从提出问题,到寻找源码,然后是深入分析源码(还可以更深入),直至最后完成了自定义开机动画这一个功能。文章里面包含了很多自己的思考以及分析过程,如果是想要直接知道如何修改开机动画的话,可以直接看第七小节的内容。
接下来其实还有很多可以继续改进的地方:一是增加系统属性,帮助我们自定义是否开启自定义动画的功能。二就是源码中是如何通过OpenGL实现两张图片完成Android原生开机动画的。本文章的开机动画为了效果更好,采用了很多序列帧,占了比较大的内存。三是分析加入新的开机动画后,开机时间的变化,可以使用perfetto工具来进行分析。后面的博客可能会继续深入进行研究,敬请期待!
推荐阅读:
我的新书,《第一行代码 第3版》已出版!
提升 WebView 用户体验的关键:Android WebChromeClient 解析
Now in Android !AndroidApp开发的最佳实践,让我看看是怎么个事?
欢迎关注我的公众号
学习技术或投稿
长按上图,识别图中二维码即可关注