理解VSync-1-软件VSync及节拍器

1. 前言

忽然有一天,我想要做一件事:去代码中去验证那些曾经被"灌输"的理论。

-- 服装学院的IT男

本篇为 VSync 系列的第一篇,主要介绍软件 VSync 和理解节拍器。

本系列为之前学习 SurfaceFlinger 整理的一些笔记,现在分享出来,希望能帮助到有需要的同学。 代码基于 Android 13 ,虽然最新源码源码中部分逻辑已经了,但总体思路还是没有变的,不影响对 VSync 整体逻辑的理解。

VSync 系列目录:

理解VSync-1-软件VSync及节拍器

1. SW-VSync简介

为什么不直接使用硬件 VSync (HW-VSYNC)?个人认为有以下两个原因:

    1. 如果 SurfaceFlinger 和每个应用都去监听硬件 VSync(HW-VSYNC) 无论从功耗上还是从模块设计上都是不妥的。
    1. 硬件(HW-VSYNC)是周期性的,因此软件层完全有可行性去模拟出一个等频率的软件 VSync(SW-VSYNC),于是有了下面这张图。

这张图来自 Google 官网(source.android.com/docs/core/g...

有一个"DispSync"可以基于 HW-VSYNC 来计算出对应的 SW-VSYNC ,这样软件层就接收 SW-VSYNC 最对应处理就好。 至于这里提到的"相位偏移","软件锁相回路 "是啥,暂时可以忽略,以黑盒的形式知道"DispSync"能算出 SW-VSYNC 即可。

这幅图中,"DispSync"计算的 SW-VSYNC 有两类:

  1. Choreographer 使用的 VSYNC
  2. SurfaceFlinger 使用的 SF_VSYNC

Choreographer 我们知道是 Java 层的一个类,控制应用绘制 UI 刷新节奏的,所以这里的第一种 VSYNC 就是给应用的。 第二种 SF_VSYNC 则是给 SurfaceFlinger 控制 Layer 合成的,

1.1 SW-VSync模型与计算

这块的理解可复杂可简单,复杂起来就是要去看模型的计算规则,是要哪些参数,相位差是多少,怎么计算的 SW-VSYNC 。既然是模拟的 HW-VSYNC 那必然还会有误差,出现误差后什么如何纠正等等。 但是如果不是从事 SurfaceFlinger 模块开发或者是刚了解这块的,建议以黑盒形式简单的了解这块。

    1. Google是以线性方程: y=bx+a 的方式来计算 SW-VSYNC
    1. 有一个反馈机制,当 SW-VSYNC 与 HW-VSYNC 差距达到阈值就开始重新采样计算纠正

1.2 SW-VSync 的分类

从上面的图可以看出 SW-VSync 分为了两类,这里再结合 Trace 看一下真正的 SW-VSync 有哪些:

    1. VSYNC-app :控制APP的UI渲染
    1. VSYNC-appsf :给APP用,感觉是用于调试的,暂时可以忽略这个
    1. VSYNC-sf : 控制SurfaceFlinger合成Layer

这里比较容易疑惑的是多出来了个 VSYNC-appsf ,这个目前也不太了解,因为 T 的代码对这块有重构,所以新增了这个 VSYNC-appsf ,感觉像是用于调试的,不过可以目前可以忽略这个,不影响后面的分析。 感兴趣的可以看[马哥的文章](blog.csdn.net/learnframew...

1.3 SW-VSync的产生

这里可能有三个问题:

    1. HW-VSync 是周期性一直存在的,那 SW-VSync 也一直存在吗?
    1. SW-VSync 是怎么分类的
    1. VSYNC-app ,VSYNC-appsf 和 VSYNC-sf 是同时触发的吗?

这三个问题将在本系列文章中一一解答,但目前需要先知道另一件事:SW-VSync 是如何产生的?

先抛开代码,举个栗子: 会看这篇文章的应该都是程序员,且在一线城市。以我所在的上海为例,每天上下班都要坐地铁,地铁的时间是很准时的,假设他的发车时间是全天固定的(早晚高峰肯定固定),以2分钟一趟为例,如果知道他的首发时间,知道某一趟到站的时间,是不是就可以计算到每一趟地铁到达你要坐的那个站的时间。

这个时间启动高德地图都能帮你算好了,所以只要打开地图,你就可以看到下一趟到站的地铁是什么时候,甚至下面几趟的都可以算好。不用去地铁站看站牌信息,在手机上就可以看到地铁到站的时间。

这个地铁到站的时间计算,和软件模型计算 SW-VSync 是类似的。

如果让程序员写一个逻辑,地铁要到站的时候就手机振动,或者发送一个Notification 通知用户,那你怎么做?

写个定时器呗,根据当前时间,和已知的下一趟地铁来的时间,计算出时间差,传给定时器,定时结束就可以做相应的事情通知用户了。

SW-VSync 的产生其实也就是通过定时器实现的,如果有应用或者 SurfaceFlinger 需要 VSync,就会发送请求,软件根据发送请求的时间,也就是当前时间(now)和它所预测的下一次 HW-VSync 时间,就能计算出还有多久 HW-VSync 要来了,假设是 10ms,那就开始设置一个 10ms 的定时任务,定时一到就触发回调,该干啥干啥。比如是 SurfaceFlinger 请求的 VSYNC 那就通知 SurfaceFlinger ,VSync 来了就可以开始干活了(合成Layer), 这个 VSync 就是 VSYNC-sf 。假设是应用请求的 VSync ,那定时结束就通知 app ,这个 VSync 就是 VSYNC-app 。 当然也可能2个同时申请了,那就都通知到,也就是既触发 VSYNC-sf 也触发 VSYNC-app 。

这里的描述,可能不是完全正确,硬扣细节肯定也是不严谨的,但是对于刚开始接触 VSync 来说,这样理解我觉得是可以先有这么一个概念,然后再通过实际的源码加深印象。

SW-VSync 是按需产生的,谁请求就给谁,都不请求就没人触发定时器了,自然也就不产生 SW-VSync 了。

一般来说肯定是 app 先请求绘制,绘制完了 SurfaceFlinger 再请求 VSync 来合成,所以绝大部分情况SurfaceFlinger 请求 VSync 的时候,定时器已经被 app 的请求触发了,所以 SurfaceFlinger 只需要等定时结束就好。

另外这里说的 app 是代表所有应用,而不是指某个应用,app 先收到 VSYNC-app ,然后再发送给具体需要的应用。

整个框架模型如下图所示:

其中 VSYNC-sf 是发送给 SurfaceFlinger 让其做 layer 的合成, VSYNC-app 则是还需要再做一次分发,只有具体的应用请求了才需要分发给这个应用。而应用端是由Choreographer来处理 Vsync 相关的事务。

这里的 VSYNC-appsf 用于测试,可以忽略,后面的分析一般也不会提到。

2. SW-VSYNC 的节拍器 VsyncDispatch 创建

前面提到 SW-VSYNC 其实目前有三个监听者: [VSYNC-app , VSYNC-appsf , VSYNC-sf] ,也大概介绍了 SW-VSYNC 的概念,那么撸代码先分析这个 3 个监听是如何注册的。

SW-VSync 模块中存在一个很重要的类,它就是号称 SW-VSYNC 的节拍器(VsyncDispatch),也有叫它信号泵的,顾名思义它会控制软件 VSync 的分发。

上面提到的三个监听都会被注册到 VsyncDispatch 子类 VSyncDispatchTimerQueue 下的 mCallbacks 对象中。

这一小节先来了解一下这个节拍器。

下面开始看代码:

开机过程中会启动 SurfaceFlinger 进程,经过一系列调用后会执行到 SurfaceFlinger::initScheduler 方法。

scss 复制代码
# SurfaceFlinger.cpp

    void SurfaceFlinger::initScheduler(const sp<DisplayDevice>& display) {
        // 避免重复初始化
        if (mScheduler) {
            mScheduler->setRefreshRateConfigs(display->holdRefreshRateConfigs());
            return;
        }
        // 是一个Fps对象,其中存储了刷新率fps和刷新周期period
        const auto currRefreshRate = display->getActiveMode()->getFps();
        mRefreshRateStats = std::make_unique<scheduler::RefreshRateStats>(*mTimeStats, currRefreshRate,
                                                                        hal::PowerMode::OFF);
        // 封装了不同刷新率下的VSync配置信息
        mVsyncConfiguration = getFactory().createVsyncConfiguration(currRefreshRate);
        mVsyncModulator = sp<VsyncModulator>::make(mVsyncConfiguration->getCurrentConfigs());

        ......
        // 重点* 1.1 创建 Scheduler
        mScheduler = std::make_unique<scheduler::Scheduler>(static_cast<ICompositor&>(*this),
                                                            static_cast<ISchedulerCallback&>(*this),
                                                            features);
        {
            auto configs = display->holdRefreshRateConfigs();
            if (configs->kernelIdleTimerController().has_value()) {
                features |= Feature::kKernelIdleTimer;
            }
            // 重点* 1.2 初始化 VsyncSchedule
            mScheduler->createVsyncSchedule(features);
            mScheduler->setRefreshRateConfigs(std::move(configs));
        }
        setVsyncEnabled(false);
        mScheduler->startTimers();

        const auto configs = mVsyncConfiguration->getCurrentConfigs();
        const nsecs_t vsyncPeriod = currRefreshRate.getPeriodNsecs();
        // 重点* 2.1 app 注册回调
        mAppConnectionHandle =
                mScheduler->createConnection("app", mFrameTimeline->getTokenManager(),
                                            /*workDuration=*/configs.late.appWorkDuration,
                                            /*readyDuration=*/configs.late.sfWorkDuration,
                                            impl::EventThread::InterceptVSyncsCallback());
        // 重点* 2.2 appSf 注册回调
        mSfConnectionHandle =
                mScheduler->createConnection("appSf", mFrameTimeline->getTokenManager(),
                                            /*workDuration=*/std::chrono::nanoseconds(vsyncPeriod),
                                            /*readyDuration=*/configs.late.sfWorkDuration,
                                            [this](nsecs_t timestamp) {
                                                mInterceptor->saveVSyncEvent(timestamp);
                                            });
        // 重点* 2.3 sf 注册回调
        mScheduler->initVsync(mScheduler->getVsyncDispatch(), *mFrameTimeline->getTokenManager(),
                            configs.late.sfWorkDuration);
        ......
    }

这里一共分为两大步:

    1. Scheduler 和 VsyncSchedule 的初始化
    1. app ,appsf,sf 注册回调

app ,appsf,sf 对应到 Trace 上就是 VSYNC-app、VSYNC-apps、VSYNC-sf

调用链:

arduino 复制代码
SurfaceFlinger::initScheduler
    Scheduler::init                   -- 创建 Scheduler
    Scheduler::createVsyncSchedule    -- 创建 VsyncSchedule
    Scheduler::createConnection       -- app appsf 注册回调
    MessageQueue::initVsync           -- sf 注册回调

2.1 Scheduler 和 VsyncSchedule 的初始化

在 SurfaceFlinger::initScheduler 方法的1.1处创建 Scheduler。

随后在1.2处执行了 Scheduler::createVsyncSchedule 方法,看方法名是创建一个 VsyncSchedule 。 这里涉及到 C++17 的语法,mVsyncSchedule 是定义在头文件的变量,然后执行 emplace 时会用参数用提供的 features 参数来触发 VsyncSchedule 对象的构建。

arduino 复制代码
# Scheduler.h
    std::optional<VsyncSchedule> mVsyncSchedule;

# Scheduler.cpp
    void Scheduler::createVsyncSchedule(FeatureFlags features) {
        mVsyncSchedule.emplace(features);
    }

Scheduler 和 VsyncSchedule 都构建了,其中 VsyncSchedule 下面有三个重要的类。

ini 复制代码
# VsyncSchedule.h

    class VsyncSchedule {
        ......
    private:
        // 定义别名
        using TrackerPtr = std::unique_ptr<VsyncTracker>;
        using DispatchPtr = std::unique_ptr<VsyncDispatch>;
        using ControllerPtr = std::unique_ptr<VsyncController>;
        ......
        // 定义三个对象
        TrackerPtr mTracker;
        DispatchPtr mDispatch;
        ControllerPtr mController;
    }

# VsyncSchedule.cpp
    // 构建时调用
    VsyncSchedule::VsyncSchedule(FeatureFlags features)
        : mTracker(createTracker()),
            mDispatch(createDispatch(*mTracker)),
            mController(createController(*mTracker, features)) {
        if (features.test(Feature::kTracePredictedVsync)) {
            mTracer = std::make_unique<PredictedVsyncTracer>(*mDispatch);
        }
    }

可以看到 VsyncSchedule 构建触发了三个重要的对象创建:

    1. VsyncTracker -- 训练软件 VSync 模型
    1. VsyncDispatch -- 节拍器,控制软件 VSync 的回调
    1. VsyncController -- 传递HWVsync,presentFence信号

这3个类在整个软件 VSync 的模块中是极其重要的,其中 VsyncDispatch 就是控制软件 VSync 的回调,这个很重要,其子类为:VSyncDispatchTimerQueue 。 目前先只看这个类的创建流程。

2.2 Vsync 节拍器的创建(VsyncDispatch)

创建 VsyncDispatch 的方法为 VsyncSchedule::createDispatch ,代码如下:

arduino 复制代码
# VsyncSchedule.cpp

    VsyncSchedule::DispatchPtr VsyncSchedule::createDispatch(VsyncTracker& tracker) {
        using namespace std::chrono_literals;

        // TODO(b/144707443): Tune constants.
        constexpr std::chrono::nanoseconds kGroupDispatchWithin = 500us;
        constexpr std::chrono::nanoseconds kSnapToSameVsyncWithin = 3ms;
        // * 实际创建的是 VSyncDispatchTimerQueue
        return std::make_unique<VSyncDispatchTimerQueue>(std::make_unique<Timer>(), tracker,
                                                        kGroupDispatchWithin.count(),
                                                        kSnapToSameVsyncWithin.count());
    }
    1. 传递了四个参数,第一个参数为构建了一个 Timer 对象,第二个参数传递了 VsyncTracker ,后面两个为时间,暂时不管。
    1. 实际上返回的是 VSyncDispatchTimerQueue 对象,其是 VsyncSchedule 子类
css 复制代码
# VSyncDispatchTimerQueue.cpp
    VSyncDispatchTimerQueue::VSyncDispatchTimerQueue(std::unique_ptr<TimeKeeper> tk,
                                                    VSyncTracker& tracker, nsecs_t timerSlack,
                                                    nsecs_t minVsyncDistance)
        : mTimeKeeper(std::move(tk)),
            mTracker(tracker),
            mTimerSlack(timerSlack),
            mMinVsyncDistance(minVsyncDistance) {}

2.2.1 定时器 Timer

构建 VSyncDispatchTimerQueue 的时候传了一个 Timer 对象,Timer 是 TimeKeeper 的子类,然后被赋值给 VSyncDispatchTimerQueue 的 mTimeKeeper 变量,VsyncTracker 被赋值给 mTracker 。

这个 Timer 很重要,内部创建了一个线程,线程名为 "TimerDispatch" 专门控制软件层的 VSync ,节拍器的由来也是因为这个功能。

scss 复制代码
# Timer.cpp
    Timer::Timer() {
        reset();
        // 执行 threadMain 方法
        mDispatchThread = std::thread([this]() { threadMain(); });
    }

Timer 的父类是 TimeKeeper ,其父类是 Clock 的子类。

2.2.2 VSync 的定时的申请与结束处理

软件 VSync 的产生其实是靠一个定时器,也就是上的 Timer,既然是定时器那就必定有以下两个操作:

    1. 触发定时任务的方法
    1. 定时结束后的回调处理

这两个方法都定义在 VsyncDispatch 的子类 VSyncDispatchTimerQueue 中,对应代码如下:

javascript 复制代码
# VSyncDispatchTimerQueue.cpp

    // 申请Vsync(本质就是触发定时任务)
    ScheduleResult VSyncDispatchTimerQueue::schedule(CallbackToken token,
                                                    ScheduleTiming scheduleTiming) {
            ......// 触发定时任务
        }

    // 定时结束执行
    void VSyncDispatchTimerQueue::timerCallback() {
        ....... // 遍历 mCallbacks 发送Vsync
    }

这两个方法后面会详细介绍,应用和 SurfaceFlinger 如果想要接收到 VSync 信号则需要先申请 VSync,而申请 VSync 的流程必须要走到 VSyncDispatchTimerQueue::schedule 方法。

这个方法方法的最重要的事就是根据参数,触发一个定时任务。

定时结束后,其实就可以理解为马上要发送 SW-VSYNC 了,VSyncDispatchTimerQueue::timerCallback 方法会根据条件触发对应的 VSync 发送。

2.2.3 VSyncDispatchTimerQueue下 mCallbacks 的定义

"根据条件触发对应的 VSync 发送"是什么意思呢? 首先软件 VSync 是按需发送的,谁请求了才发给谁,所以会根据"条件"发送。 另外我们知道有 Trace 上就是 VSYNC-app、VSYNC-appsf、VSYNC-sf 三个 SW-VSYNC,也就是在 SurfaceFlinger::initScheduler 方法里看到触发了 app,appsf,sf 三个回调的注册。

这三个最终是注册到 VSyncDispatchTimerQueue 下定义的 mCallbacks 中,这个变量的定义如下:

arduino 复制代码
# VSyncDispatchTimerQueue.h

    class VSyncDispatchTimerQueue : public VSyncDispatch {
    ......
    private:

        using CallbackMap =
            std::unordered_map<CallbackToken, std::shared_ptr<VSyncDispatchTimerQueueEntry>>;
        CallbackMap mCallbacks GUARDED_BY(mMutex);
    }

mCallbacks 的原来是一个键值对, key 为唯一标识 token,value 是 VSyncDispatchTimerQueueEntry 类型。

在 VSyncDispatchTimerQueue::timerCallback 方法中就是遍历 mCallbacks 下的元素,按需触发对应的回调,也就是触发响应的 VSYNC-app VSYNC-appsf VSYNC-sf 。

2.3 Vsync 节拍器小结

VSyncDispatchTimerQueue 相关的 UML 类图如下:

这里只是对 VSyncDispatchTimerQueue 这个类做了简单的介绍,后面的文章中还会看到其更多的东西,

    1. VSyncDispatchTimerQueue 是节拍器 VsyncDispatch 的子类
    1. VsyncDispatch 内部有个定时器(Timer)对象,维护了一个叫 "TimerDispatch" 专门控制软件层的 VSYNC
    1. 应用和 SurfaceFlinger 想要接收到 Vsync 都必须通过 VSyncDispatchTimerQueue::schedule 来申请(触发定时任务)
    1. 定时结束后触发 VSyncDispatchTimerQueue::timerCallback 方法
    1. VSyncDispatchTimerQueue 内部有个 mCallbacks 集合维护了对 SW-VSync 感兴趣的回调,也就是 应用(app)、SurfaceFlinger(sf)、和 appsf
    1. 通过 VSyncDispatchTimerQueue::registerCallback 方法将回调注册到 mCallbacks 中,这个方法定义在父类中
    1. VSyncDispatchTimerQueue 下面的 mCallbacks 里的元素是 VSyncDispatchTimerQueueEntry (这个类也很复杂,目前类图里先省略内容)

3. 补充:dump 软件 Vsync 回调集合

虽然还没分析具体代码,但是已经多次给出结论:app appsf sf 都在 VSyncDispatchTimerQueue::mCallbacks 下。 为了确认和再加深这个结论,可以使用以下命令对 SurfaceFlinger 进行 dump:

复制代码
adb shell dumpsys SurfaceFlinger >sf.txt

输出和回调相关的内容如下:

yaml 复制代码
VsyncDispatch:
	Timer:
		DebugState: Waiting
	...... // 忽略

	Callbacks:
		appSf:  
			workDuration: 16.67ms readyDuration: 27.60ms earliestVsync: -525582.81ms relative to now
			mLastDispatchTime: 525566.12ms ago
		sf:  
			workDuration: 27.60ms readyDuration: 0.00ms earliestVsync: -56083.53ms relative to now
			mLastDispatchTime: 55935.26ms ago
		app:  
			workDuration: 20.00ms readyDuration: 27.60ms earliestVsync: -32569.80ms relative to now
			mLastDispatchTime: 32553.14ms ago

可以看到 "Callbacks:" 下有三个对象,就是上面提到的 app、appsf、sf。

对应控制输出的代码如下:

scss 复制代码
# VSyncDispatchTimerQueue.cpp
    void VSyncDispatchTimerQueue::dump(std::string& result) const {
        std::lock_guard lock(mMutex);
        StringAppendF(&result, "\tTimer:\n");
        mTimeKeeper->dump(result);
        StringAppendF(&result, "\tmTimerSlack: %.2fms mMinVsyncDistance: %.2fms\n", mTimerSlack / 1e6f,
                    mMinVsyncDistance / 1e6f);
        StringAppendF(&result, "\tmIntendedWakeupTime: %.2fms from now\n",
                    (mIntendedWakeupTime - mTimeKeeper->now()) / 1e6f);
        StringAppendF(&result, "\tmLastTimerCallback: %.2fms ago mLastTimerSchedule: %.2fms ago\n",
                    (mTimeKeeper->now() - mLastTimerCallback) / 1e6f,
                    (mTimeKeeper->now() - mLastTimerSchedule) / 1e6f);
        StringAppendF(&result, "\tCallbacks:\n");
        // 内容就是 mCallbacks 集合下的内容
        for (const auto& [token, entry] : mCallbacks) {
            entry->dump(result);
        }
}

我们已经知道这个 mCallbacks 下的元素就是3个 VSyncDispatchTimerQueueEntry 实例,所以具体的 dump 要看 VSyncDispatchTimerQueueEntry 的实现。 VSyncDispatchTimerQueueEntry 定义在 VSyncDispatchTimerQueue.cpp 下面。

swift 复制代码
# VSyncDispatchTimerQueue.cpp
    void VSyncDispatchTimerQueueEntry::dump(std::string& result) const {
        std::lock_guard<std::mutex> lk(mRunningMutex);
        std::string armedInfo;
        if (mArmedInfo) {
            StringAppendF(&armedInfo,
                        "[wake up in %.2fms deadline in %.2fms for vsync %.2fms from now]",
                        (mArmedInfo->mActualWakeupTime - systemTime()) / 1e6f,
                        (mArmedInfo->mActualReadyTime - systemTime()) / 1e6f,
                        (mArmedInfo->mActualVsyncTime - systemTime()) / 1e6f);
        }

        StringAppendF(&result, "\t\t%s: %s %s\n", mName.c_str(),
                    mRunning ? "(in callback function)" : "", armedInfo.c_str());
        StringAppendF(&result,
                    "\t\t\tworkDuration: %.2fms readyDuration: %.2fms earliestVsync: %.2fms relative "
                    "to now\n",
                    mScheduleTiming.workDuration / 1e6f, mScheduleTiming.readyDuration / 1e6f,
                    (mScheduleTiming.earliestVsync - systemTime()) / 1e6f);

        if (mLastDispatchTime) {
            StringAppendF(&result, "\t\t\tmLastDispatchTime: %.2fms ago\n",
                        (systemTime() - *mLastDispatchTime) / 1e6f);
        } else {
            StringAppendF(&result, "\t\t\tmLastDispatchTime unknown\n");
        }
    }

4. 总结

本篇介绍了个人对 SW-Vsync 的理解,另外重点介绍了 VsyncDispatch 和其子类 VSyncDispatchTimerQueue 。

下一篇将详细分析 SurfaceFlinger::initScheduler 方法中三个对象(app, appsf, sf)是如何把自己注册到 VSyncDispatchTimerQueue 下的 mCallbacks 中的。

本篇涉及到的调用链如下:

arduino 复制代码
SurfaceFlinger::initScheduler
    Scheduler::init                 -- Scheduler初始化
    Scheduler::createVsyncSchedule  -- VsyncSchedule 初始化
        VsyncSchedule::emplace
            VsyncSchedule::init
                VsyncSchedule::createTracker           -- VsyncTracker
                VsyncSchedule::createDispatch          -- VsyncDispatch(节拍器)
                    Timer::Timer                       -- 定时器创建
                    VSyncDispatchTimerQueue::init  
                VsyncSchedule::createController        -- VsyncController
    Scheduler::createConnection     -- app appsf 注册回调
    MessageQueue::initVsync      -- sf 注册回调

参考:

<千里马学框架-SurfaceFlinger>

www.jianshu.com/p/5e9c558d1...

source.android.com/docs/core/g...

相关推荐
用户2018792831672 小时前
ANR之RenderThread不可中断睡眠state=D
android
煤球王子2 小时前
简单学:Android14中的Bluetooth—PBAP下载
android
小趴菜82272 小时前
安卓接入Max广告源
android
齊家治國平天下2 小时前
Android 14 系统 ANR (Application Not Responding) 深度分析与解决指南
android·anr
ZHANG13HAO2 小时前
Android 13.0 Framework 实现应用通知使用权默认开启的技术指南
android
【ql君】qlexcel2 小时前
Android 安卓RIL介绍
android·安卓·ril
写点啥呢2 小时前
android12解决非CarProperty接口深色模式设置后开机无法保持
android·车机·aosp·深色模式·座舱
IT酷盖2 小时前
Android解决隐藏依赖冲突
android·前端·vue.js
努力学习的小廉4 小时前
初识MYSQL —— 数据库基础
android·数据库·mysql
风起云涌~4 小时前
【Android】浅谈androidx.startup.InitializationProvider
android