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

Android 10 (Android Q)中的屏幕刷新率(display refresh rate)切换方法和策略

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

北京

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

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

看不清楚,换张图片

免费获取短信验证码

Android 10 (Android Q)中的屏幕刷新率(display refresh rate)切换方法和策略

       本文禁止转载,如有需求,请联系作者。

1. 屏幕刷新率和应用的显示帧率

       首先请区分好屏幕刷新率(Panel Refresh Rate)和应用的帧率(APP Frame Rate)两个概念。

       屏幕刷新率是显示器(LCD/OLED等)的硬件性能参数,即它每秒钟能刷新几幅画面,一般用物理单位Hz来表示。我们知道显示器的面板始由物理像素铺满的。比如目前的手机屏幕,主流是1080P的分辨率,那么就是1080*1920个像素点,即每行1080个像素,每列1920个像素。我们一般把显示器画完一行的1080个像素叫做行扫描,把1920行全部画完叫做场扫描,把开始扫描一行的物理信号叫做水平同步信号(HSYNC),把开始扫描一场的物理信号叫做垂直同步信号(VSYNC)。这里的VSYNC就是跟我们要讲的屏幕刷新率直接相关的了。显示器每秒钟能画完几场,就会有多少个VSYNC信号,称为场频,也就是屏幕的刷新率是多少。比如目前大部分手机屏幕的刷新率是60Hz,也就意味着它们每秒钟产生60个VSYNC信号。进而又衍生出另一个概念即场同步周期(vsync period),对于60Hz刷新率的屏幕来说,vsync period就是1000/60=16.6ms。

       那什么是应用显示帧率呢?可以认为它是应用画图的速度,一般用fps(frames per second)来表示。比如在微信的聊天界面上下滑动屏幕,微信界面的聊天内容也跟随我们的手指上下移动。假设我们正好要在一秒内往上移动一屏的内容,那么这一屏的内容移动过程在一秒内更新了多少次,是微信应用开发者可以控制的。如果一秒内只更新两三次,你会看到明显的画面丁顿,而如果更新24次以上,你一般就感觉不到卡顿,除非你不是人,是神:)这里应用更新界面的次数,或者应用画图的次数,我们可以理解为应用的显示帧率。当然应用的显示帧率不是越快越好,超过了屏幕刷新率,系统还要做pulldown,反而增加系统的负担。而且应用显示帧率是一个动态的量,因为应用界面并不是总是在变化,当你看新闻的时候,不拖动屏幕,那么显示内容是静态的,当你看一个动画的时候,这个动画的源文件可能是15fps的,也可能是30fps的,也会影响到应用显示帧率。

2. 应用程序(app)与刷新率(refresh rate)

       上面我们讲了屏幕刷新率和应用显示帧率的概念。现在来看在应用显示设计中,他们的关系。

        在应用开发特别是游戏的设计中,有时候对屏幕刷新率是很敏感的。

        这里有几个数据,在60Hz刷新率时,场同步周期(vsync period)是1000/60=16.6ms;而在90Hz刷新率时,场同步周期变成1000/90=11.1ms。也就是说,如果应用想要达到跟刷新率相同的显示帧率,在60Hz的时候,他只要在16.6毫秒内画完一幅图就行,而如果想在90Hz刷新率时跟上屏幕刷新率的话,它就只有11.1毫秒的画图时间了。很显然对于动画或界面比较复杂的应用来说,如果在60Hz的时候已经比较吃力的话,在90Hz的时候就必须调整自己的画图方式和步骤,优化自己的效率。从另一个角度看,原来应用程序画动画的时候,只要是能跟60Hz的分频频率合拍(比如15/20/30/...fps),就能保证不会产生抖动(jitter)。但是,如果Android设备运行在90Hz刷新率的话,则合适的数字变成(15/30/45/...)。也就是说,如果应用开发工程师以20fps画图的话,在90Hz刷新率的时候是不合适的,会造成抖动。这就需要应开开发工程师知道Android设备的屏幕刷新率,并根据屏幕刷新率来调整自己的画图速度。或者,实在没办法的话,想办法让设备把屏幕刷新率改成合适的那个。

      以前,因为绝大部分Android设备屏幕的显示屏都是60Hz的刷新率,所以游戏开发工程师都会假设他们的应用是运行在60Hz的设备上,从而以在60Hz的刷新率下流畅运行作为开发目标,游戏动画的设计也理论上要求以60的倍频或者分频来设计,比如15/20/30/60/120fps,这样在手机上render的时候正好扣紧vsync的鼓点而不会造成抖动。所以应用设计者不会关心手机的屏幕刷新率是否会改变。但是随着手机屏幕硬件的不断升级,新的机型越来越多的开始支持更高的刷新率,比如90/120Hz。如果这时候仍然按照以前的开发经验来设计应用的话,一来无法体现高刷新率屏幕的优势,二来因为刷新率提升导致vsync period画图时间缩短,可能出现掉帧的情况。

       为了跟上技术的更新,应用开发工程师需要从以下4点来优化自己的代码(可参考“多屏幕刷新率(refresh rate)模式下的帧显示流水线”一文):

           1.  想办法提升画图的效率,保证在更短的vsync period中画完一幅图,可以引入并行线程;

           2.  增加自己的render pipe line;

           3.  监测设备屏幕刷新率的变化,根据刷新率的变化调整应用的画图速度;

           4.  调用系统接口改变设备的刷新率(如果设备支持的话)。

       对于屏幕刷新率只有一个的Android设备,应用的适配相对简单。只要使用Display.getSupportedModes获取设备的刷新率,然后做适配就行。对于支持多个屏幕刷新率的设备,在Android Q上对目前对屏幕刷新率的接口较少。可以使用displayListener或注册回调来获取,或者通过WindowManager.LayoutParams.preferredDisplayModeId来控制。Google在Pixel 4上实现了自动的屏幕刷新率切换,可以根据场景变化在60Hz和90Hz之间自适应的切换。

       如果只是想熟悉,可以试试下面的命令来看看不同刷新率下的效果。它并不是把刷新率固定在某个值,只是限定了最高刷新率。如果想同时体验固定帧率和自动帧率,可以参考的手机型号有Motorola Edge和Nubia Red Magic 3。

       adb shell settings put system peak_refresh_rate 90.0/60.0

3. Framework层的刷新率接口和策略

       Google已经在Android Q(10.0)上实现了对屏幕刷新率的操控策略,代码主要在DisplayManagerService和SurfaceFlinger,一个是Java层,一个是native层。

3.1 Java层相关代码

       DisplayManagerService通过DisplayModeDirector来管理屏幕刷新率。当系统启动ready的时候,DisplayModeDirector会注册。

       frameworks/base/services/core/java/com/android/server/display/DisplayManagerService.java

447      
450      public void systemReady(boolean safeMode, boolean onlyCore) {
451          synchronized (mSyncRoot) {
452              mSafeMode = safeMode;
453              mOnlyCore = onlyCore;
454              mSystemReady = true;
455              // Just in case the top inset changed before the system was ready. At this point, any
456              // relevant configuration should be in place.
457              recordTopInsetLocked(mLogicalDisplays.get(Display.DEFAULT_DISPLAY));
458          }
459  
460          mDisplayModeDirector.setListener(new AllowedDisplayModeObserver());
461          mDisplayModeDirector.start();
462  
463          mHandler.sendEmptyMessage(MSG_REGISTER_ADDITIONAL_DISPLAY_ADAPTERS);
464      }       

       而DisplayModeDirector会通过mSettingsObserver和mDisplayObserver两个观察哨来监测setting中用户操作对刷新率的改变和设备显示硬件的变动(比如第二块显示屏幕,或者外接了显示设备,投影等)导致的刷新率改变。

84      public DisplayModeDirector(@NonNull Context context, @NonNull Handler handler) {
85          mContext = context;
86          mHandler = new DisplayModeDirectorHandler(handler.getLooper());
87          mVotesByDisplay = new SparseArray();
88          mSupportedModesByDisplay = new SparseArray();
89          mDefaultModeByDisplay =  new SparseArray();
90          mAppRequestObserver = new AppRequestObserver();
91          mSettingsObserver = new SettingsObserver(context, handler);
92          mDisplayObserver = new DisplayObserver(context, handler);
93      }
 

       SettingObserver的onChange处理函数,和DisplayObserver的onDisplayAdded/onDisplayRemoved/onDisplayChanged处理函数,就是监测setting中用户的操作,和设备显示硬件变化的。

540          public void onChange(boolean selfChange, Uri uri, int userId) {
541              synchronized (mLock) {
543                  if (mRefreshRateSetting.equals(uri)) {
545                      updateRefreshRateSettingLocked();
546                  } else if (mLowPowerModeSetting.equals(uri)) {
547                      updateLowPowerModeSettingLocked();
548                  }
549              }
550          }

690          @Override
691          public void onDisplayAdded(int displayId) {
692              updateDisplayModes(displayId);
693          }
694  
695          @Override
696          public void onDisplayRemoved(int displayId) {
697              synchronized (mLock) {
698                  mSupportedModesByDisplay.remove(displayId);
699                  mDefaultModeByDisplay.remove(displayId);
700              }
701          }
702  
703          @Override
704          public void onDisplayChanged(int displayId) {
705              updateDisplayModes(displayId);
706          }
 

       他们都会调用到DisplayModeDirector的notifyAllowedModesChangedLocked()。

371      private void notifyAllowedModesChangedLocked() {
372          if (mListener != null && !mHandler.hasMessages(MSG_ALLOWED_MODES_CHANGED)) {
373              // We need to post this to a handler to avoid calling out while holding the lock
374              // since we know there are things that both listen for changes as well as provide
375              // information. If we did call out while holding the lock, then there's no guaranteed
376              // lock order and we run the real of risk deadlock.
377              Message msg = mHandler.obtainMessage(MSG_ALLOWED_MODES_CHANGED, mListener);
378              msg.sendToTarget();
379          }
380      }

409          public void handleMessage(Message msg) {
410              switch (msg.what) {
411                  case MSG_ALLOWED_MODES_CHANGED:
412                      Listener listener = (Listener) msg.obj;
413                      listener.onAllowedDisplayModesChanged();
414                      break;
415              }
416          }  
 

       并最终调用DisplayManagerService的onAllowedDisplayModesChangedInternal()。其中display.setAllowedDisplayModesLocked则会通过SurfaceControl.setAllowedDisplayConfigs传递给native层SurfaceFlinger的屏幕刷新率Schedule策略模块。

1422      private void onAllowedDisplayModesChangedInternal() {
1423          boolean changed = false;
1424          synchronized (mSyncRoot) {
1425              final int count = mLogicalDisplays.size();
1426              for (int i = 0; i < count; i++) {
1427                  LogicalDisplay display = mLogicalDisplays.valueAt(i);
1428                  int displayId = mLogicalDisplays.keyAt(i);
1429                  int[] allowedModes = mDisplayModeDirector.getAllowedModes(displayId);
1430                  // Note that order is important here since not all display devices are capable of
1431                  // automatically switching, so we do actually want to check for equality and not
1432                  // just equivalent contents (regardless of order).
1433                  if (!Arrays.equals(allowedModes, display.getAllowedDisplayModesLocked())) {
1434                      display.setAllowedDisplayModesLocked(allowedModes);
1435                      changed = true;
1436                  }
1437              }
1438              if (changed) {
1439                  scheduleTraversalLocked(false);
1440              }
1441          }
1442      }

3.2 Native层相关代码

      上面讲到DisplayManagerService调用SurfaceControl.setAllowedDisplayConfigs接口来影响屏幕刷新率,通过JNI接口和libgui,直接call到了SurfaceFlinger服务.

       frameworks/base/core/jni/android_view_SurfaceControl.cpp::

                 nativeSetAllowedDisplayConfigs();

       frameworks/native/libs/gui/SurfaceComposerClient.cpp::

                 getComposerService()->setAllowedDisplayConfigs(displayToken, allowedConfigs);

       上面的getComposerService()就是得到SurfaceFlinger的服务句柄,进而调用它的接口setAllowedDisplayConfigs来对屏幕刷新率做更新。请注意这个接口里传下来的allowedConfigs是一个数组,不一定是某一个刷新率的索引。

       SurfaceFlinger通过mRefreshRateConfigs和mAllowedDisplayConfigs来维护Android设备支持的屏幕刷新率和系统目前允许使用的屏幕刷新率。比如有一款手机,它的显示屏可以支持60Hz、90Hz、120Hz三个刷新率,则mRefreshRateConfigs则记录了这3个刷新率。而应用或者系统可以通过setAllowedDisplayConfigs让SurfaceFlinger只能在某几个刷新率之间切换,就是设置的mAllowedDisplayConfigs,比如只让这个手机在60Hz和90Hz之间切换,则mAllowedDisplayConfigs就只记录了60Hz和90Hz。

       SurfaceFlinger在初始化时,会对mRefreshRateConfigs做初始化。其中getHwComposer().getConfigs是从HAL层获取设备支持的所有屏幕刷新率。

754  void SurfaceFlinger::init() {
......
890      int active_config = getHwComposer().getActiveConfigIndex(*display->getId());
891      mRefreshRateConfigs.setActiveConfig(active_config);
892      mRefreshRateConfigs.populate(getHwComposer().getConfigs(*display->getId()));
893      mRefreshRateStats.setConfigMode(active_config);
......
}

       如前面讲JAVA层代码时提到过,系统开机过程中DisplayObserver的onDisplayAdded()会被调到,进而调用setAllowedDisplayConfigs来初始化mAllowedDisplayConfigs。一般来说,开机时初始化的mRefreshRateConfigs和mAllowedDisplayConfigs是一致的,除非设备开发商做了定制,比如让设备开机默认固定在某个刷新率。比如motorola edge开机默认是auto模式,它开机后mRefreshRateConfigs和mAllowedDisplayConfigs是一致的。而motorola edge plus开机后默认是90Hz模式,它开机后mRefreshRateConfigs和mAllowedDisplayConfigs就不一致。

       为了保证从上层传过来的allowedConfigs在本机支持的刷新率内,在setAllowedDisplayConfigs时还用经过mRefreshRateConfigs的筛选。在mRefreshRateConfigs.getAllowedConfigs中,会根据设备的支持情况更新displayConfigs。

7661  void SurfaceFlinger::setAllowedDisplayConfigsInternal(const sp& display,
7662                                                        const std::vector& allowedConfigs) {
7672  
7676      // Set Phase Offsets type for the Default Refresh Rate config.
7677      RefreshRateType type = mRefreshRateConfigs.getDefaultRefreshRateType();
7678      mPhaseOffsets->setDefaultRefreshRateType(type);
7679  
7685      // Update the allowed Display Configs.
7686      mRefreshRateConfigs.getAllowedConfigs(getHwComposer().getConfigs(*display->getId()),
7687                                            &displayConfigs);
7688  
7689      mAllowedDisplayConfigs = DisplayConfigs(displayConfigs.begin(), displayConfigs.end());
7693  
7711      setPreferredDisplayConfig();
7712  }

       改变mAllowedDisplayConfigs除了使用SurfaceFlinger服务的setAllowedDisplayConfigs接口外,还可以使用SurfaceFlinger的transact code 1035。这个对调试非常有用。你可以使用类似下面的命令行来手动切换刷新率:

       adb shell service call SurfaceFlinger 1035 i32 X(X是所要切换的屏幕刷新率的索引)

       在Android Q不断迭代的后期,Google也加入了更多的直观的调试方法来让系统开发者监控设备当前的屏幕刷新率变化,比如在屏幕的左上角增加颜色方块或者数字来直观的显示当前的刷新率,同时在开发者选项中增加开关来控制这个调试功能(Android R上是肯定有的,目前Q上开发者选项里还没看到,但可以使用命令行来开关)。

       回到重点,那么,SurfaceFlinger中真正的调用底层切换屏幕刷新率的地方在哪里呢?它在刷新下一帧的准备阶段。我们继续看setAllowedDisplayConfigs接口,它最后调用了setPreferredDisplayConfig()。如果设备上enable了Google的auto切换策略,则从策略中获取最合适的刷新率,如果没有enable,则找到设备支持的最大刷新率。当然,都是在allowed的前提下。然后通过接口setDesiredActiveConfig往下调。

7638  void SurfaceFlinger::setPreferredDisplayConfig() {
7639      const auto& type = mScheduler->getPreferredRefreshRateType();
7640      const auto& config = mRefreshRateConfigs.getRefreshRate(type);
7641      if (config && isDisplayConfigAllowed(config->configId)) {
7642          ALOGV("switching to Scheduler preferred config %d", config->configId);
7643          setDesiredActiveConfig({type, config->configId, Scheduler::ConfigEvent::Changed});
7644      } else {
7645          // Set the highest allowed config by iterating backwards on available refresh rates
7646          const auto& refreshRates = mRefreshRateConfigs.getRefreshRates();
7647          for (auto iter = refreshRates.crbegin(); iter != refreshRates.crend(); ++iter) {
7648              if (iter->second && isDisplayConfigAllowed(iter->second->configId)) {
7651                  mRefreshRateConfigs.setActiveConfig(iter->second->configId);
7653          setDesiredActiveConfig({iter->first, iter->second->configId,
7654                          Scheduler::ConfigEvent::Changed});
7655                  break;
7656              }
7657          }
7658      }
7659  }

       我们继续看setDesiredActiveConfig接口。它只是把选择好的ActiveConfigInfo赋值给mDesiredActiveConfig并记一个标记mDesiredActiveConfigChanged=true。

1194  void SurfaceFlinger::setDesiredActiveConfig(const ActiveConfigInfo& info) {
......
1202      mDesiredActiveConfig = info;
1203      mDesiredActiveConfig.event = mDesiredActiveConfig.event | prevConfig;
......
1219      mDesiredActiveConfigChanged = true;
......
1225  }

       上面标记的结果的真正使用是在SurfaceFlingerm每次收到MessageQueue::INVALIDATE信号并调用performSetActiveConfig()的时候。这里首先判断mCheckPendingFence和previousFrameMissed,如果mCheckPendingFence被标记并且前一个frame没有被drop,则表示刚刚前一个frame刷新过程中改变了屏幕刷新率并成功了,这时需要调用setActiveConfigInternal去更新SF的跟显示性能相关的vsync offset等参数。如果mCheckPendingFence是false则继续判断是否需要改变屏幕刷新率,如果需要的话,则用getHwComposer().setActiveConfig发命令给HAL并标记mCheckPendingFence和触发一次INVALIDATE。

1281  bool SurfaceFlinger::performSetActiveConfig() {
1282      ATRACE_CALL();
1283      if (mCheckPendingFence) {
1284          if (previousFrameMissed()) {
1285              // fence has not signaled yet. wait for the next invalidate
1286              mEventQueue->invalidate();
1287              return true;
1288          }
1290          // We received the present fence from the HWC, so we assume it successfully updated
1291          // the config, hence we update SF.
1292          mCheckPendingFence = false;
1293          setActiveConfigInternal();
1294      }
1295  
1296      // Store the local variable to release the lock.
1303      desiredActiveConfig = mDesiredActiveConfig;
1322      mUpcomingActiveConfig = desiredActiveConfig;
1327      getHwComposer().setActiveConfig(*displayId, mUpcomingActiveConfig.configId);
1328  
1340      // we need to submit an empty frame to HWC to start the process
1341      mCheckPendingFence = true;
1342      mEventQueue->invalidate();
1343      return false;
1344  }

       不要在SurfaceFlinger中随便调用HidlHAL的setActiveConfig()去设置硬件刷新率。因为这样的话虽然硬件的刷新率变了,但是SurfaceFlinger和RenderThread并不知道,因此相关的pipeline性能相关的参数没有同步更新,会造成显示性能的未知问题。比如mVsyncModulator的setPhaseOffsets就是很重要的参数,它决定了SF和APP开始画图的针对vsync的偏移时间。只有调用SurfaceFlinger标准接口来设置刷新率,才能通过setActiveConfigInternal更新所有的刷新参数并通知关心的上层模块。

1243  void SurfaceFlinger::setActiveConfigInternal() {
1250  
1251      std::lock_guard lock(mActiveConfigLock);
1252      mRefreshRateStats.setConfigMode(mUpcomingActiveConfig.configId);
1253  
1254      display->setActiveConfig(mUpcomingActiveConfig.configId);
1255  
1256      mPhaseOffsets->setRefreshRateType(mUpcomingActiveConfig.type);
1257      const auto [early, gl, late] = mPhaseOffsets->getCurrentOffsets();
1258      mVsyncModulator.setPhaseOffsets(early, gl, late,
1259                                      mPhaseOffsets->getOffsetThresholdForNextVsync());
1260      ATRACE_INT("ActiveConfigMode", mUpcomingActiveConfig.configId);
1261  
1262      if (mUpcomingActiveConfig.event != Scheduler::ConfigEvent::None) {
1263          mScheduler->onConfigChanged(mAppConnectionHandle, display->getId()->value,
1264                                      mUpcomingActiveConfig.configId);
1265      }
1266  }

3.3 SurfaceFlinger中的屏幕刷新率切换策略

        在SurfaceFlinger的RefreshRateConfigs类中,有Google目前的屏幕刷新率相关的一些定义。比如它定义了默认的刷新率是60Hz,并定义了RefreshRateType,其中DEFAULT就对应60Hz。

35  class RefreshRateConfigs {
37      static const int DEFAULT_FPS = 60;
39  public:
44      enum class RefreshRateType {POWER_SAVING, LOW0, LOW1, LOW2, DEFAULT, PERFORMANCE, HIGH1, HIGH2};
99  }

       在SurfaceFlinger服务启动成功的时候,会调用setRefreshRateTo接口把屏幕刷新率设置为设备支持的最高刷新率。然后如果setting里有默认值的话,会通过setAllowedDisplayConfigs再改变一次。

643  void SurfaceFlinger::bootFinished()
644  {
......
698          // set the refresh rate according to the policy
699          int maxSupportedType = (int)RefreshRateType::HIGH2;
700          int minSupportedType = (int)RefreshRateType::LOW0;
701  
702          for (int type = maxSupportedType; type >= minSupportedType; type--) {
703              RefreshRateType refreshRateType = static_cast(type);
704              const auto& refreshRate = mRefreshRateConfigs.getRefreshRate(refreshRateType);
705              if (refreshRate && isDisplayConfigAllowed(refreshRate->configId)) {
706                  setRefreshRateTo(refreshRateType, Scheduler::ConfigEvent::None);
707                  return;
708              }
709          }
......
xxx  }

       等这一切都完成后,除非用户在setting里做手动操作,屏幕刷新率就不会变化了。如果想要让屏幕刷新率根据场景进行自适应的变化,就需要加入自己的策略,或者打开Google的Schedule策略。自己加策略大家可以百花齐放,我们这里说说Googel的Schedule策略。

        我们看SurfaceFlinger的init()函数,一上来就拿到一个mScheduler句柄。这个mScheduler的一项重要功能,就是管理屏幕刷新率的策略。

754  void SurfaceFlinger::init() {
755      ALOGI(  "SurfaceFlinger's main thread ready to run. Initializing graphics H/W...");
760      Mutex::Autolock _l(mStateLock);
762      mScheduler = getFactory().createScheduler([this](bool enabled) { setPrimaryVsyncEnabled(enabled); },
xxx  }

       它并没有创建一个线程去监控场景(SurfaceFlinger里的线程已经够多了),而是使用了两个timer(Q的后期又添加了一个,我们这里不讲)。一个是mIdleTimer,一个mTouchTimer。Google这个刷新率策略的初衷就是,让用户在屏幕上手动操作的时候,纵享丝滑,体验到如丝般顺滑。其他时候,是怎样就怎样吧。

       来看Scheduler的构造函数,两个timer在这里开始了他们的人生,每个人都在SF的操控下处理两件事情,resetTimerCallback和expiredTimerCallback。

60  Scheduler::Scheduler(impl::EventControlThread::SetVSyncEnabledFunction function,
61                       const scheduler::RefreshRateConfigs& refreshRateConfig)
62        : mHasSyncFramework(running_without_sync_framework(true)),
63          mDispSyncPresentTimeOffset(present_time_offset_from_vsync_ns(0)),
64          mPrimaryHWVsyncEnabled(false),
65          mHWVsyncAvailable(false),
66          mRefreshRateConfigs(refreshRateConfig) {
74  
75      mSetIdleTimerMs = set_idle_timer_ms(0);
78      mSetTouchTimerMs = set_touch_timer_ms(0);
126  
127      if (mSetIdleTimerMs > 0) {
137          mIdleTimer = std::make_unique(std::chrono::milliseconds(
138                                                                          mSetIdleTimerMs),
139                                                                  [this] { resetTimerCallback(); },
140                                                                  [this] { expiredTimerCallback(); });
142          mIdleTimer->start();
143      }
144  
145      if (mSetTouchTimerMs > 0) {
146          // Touch events are coming to SF every 100ms, so the timer needs to be higher than that
147          mTouchTimer =
148                  std::make_unique(std::chrono::milliseconds(mSetTouchTimerMs),
149                                                         [this] { resetTouchTimerCallback(); },
150                                                         [this] { expiredTouchTimerCallback(); });
151          mTouchTimer->start();
152      }
164  }

       Google把mIdleTimer和mTouchTimer的timeout时间分别设定在500毫秒和2000毫秒。这两个timer的控制逻辑具体是这样:

当用户手动在触屏上操作时,会触发notifyPowerHint,进而reset mTouchTimer,在resetTouchTimerCallback中,把刷新率类型改成PERFORMANCE(90Hz),等到mTouchTimer timeout的时候,维持当前的刷新率,等于无操作。 当屏幕显示内容有更新时,会resetIdleTimer(),而resetTimerCallback没什么操作,只有等到IdleTimer timeout时,在expiredTimerCallback()的时候,把刷新率改为DEFAULT(60Hz)。

       部分代码如下,其中handleTimerStateChanged和calculateRefreshRateType是策略的灵魂:

       当系统touch事件reset touch timer时,把mCurrentTouchState改为TouchState::ACTIVE,而在idle timer expired的时候,把mCurrentIdleTimerState改为IdleTimerState::EXPIRED。

frameworks/native/libs/gui/ISurfaceComposer.cpp
1060  status_t BnSurfaceComposer::onTransact(
1061      uint32_t code, const Parcel& data, Parcel* reply, uint32_t flags)
1062  {
1063      switch(code) {
1685          case NOTIFY_POWER_HINT: {     //从inputmanager来的事件
1686              CHECK_INTERFACE(ISurfaceComposer, data, reply);
1687              int32_t hintId;
1688              status_t error = data.readInt32(&hintId);
1689              if (error != NO_ERROR) {
1690                  ALOGE("notifyPowerHint: failed to read hintId: %d", error);
1691                  return error;
1692              }
1693              return notifyPowerHint(hintId);
1694          }
1716      }
1717  }

frameworks/native/services/surfaceflinger/SurfaceFlinger.cpp
1697  status_t SurfaceFlinger::notifyPowerHint(int32_t hintId) {
1698      PowerHint powerHint = static_cast(hintId);
1699  
1700      if (powerHint == PowerHint::INTERACTION) {
1701          mScheduler->notifyTouchEvent();   //reset touch timer
1702      }
1703  
1704      return NO_ERROR;
1705  } 

1729  void SurfaceFlinger::signalTransaction() {
1730      mScheduler->resetIdleTimer();
1731      mEventQueue->invalidate();
1732  }
1733  
1734  void SurfaceFlinger::signalLayerUpdate() {
1735      mScheduler->resetIdleTimer();
1736      mEventQueue->invalidate();
1737  }

536  void Scheduler::resetTouchTimerCallback() {
537      handleTimerStateChanged(&mCurrentTouchState, TouchState::ACTIVE, true);
538      ATRACE_INT("TouchState", 1);
539  }

528  void Scheduler::expiredTimerCallback() {
532      handleTimerStateChanged(&mCurrentIdleTimerState, IdleTimerState::EXPIRED, false);
533      ATRACE_INT("ExpiredIdleTimer", 1);
534  }

576  template
577  void Scheduler::handleTimerStateChanged(T* currentState, T newState, bool eventOnContentDetection) {
578      ConfigEvent event = ConfigEvent::None;
579      RefreshRateType newRefreshRateType;
580      {
581          std::lock_guard lock(mFeatureStateLock);
582          if (*currentState == newState) {
583              return;
584          }
585          *currentState = newState;   //改变current state
586          newRefreshRateType = calculateRefreshRateType();
587          if (mRefreshRateType == newRefreshRateType) {
588              return;
589          }
590          mRefreshRateType = newRefreshRateType;
591          if (eventOnContentDetection &&
592              mCurrentContentFeatureState == ContentFeatureState::CONTENT_DETECTION_ON) {
593              event = ConfigEvent::Changed;
594          }
595      }
596      changeRefreshRate(newRefreshRateType, event);
597  }

        下面是策略所在,在calculateRefreshRateType()中,如果发现当前mCurrentTouchState是TouchState::ACTIVE,则返回Performance,如果mCurrentIdleTimerState是IdleTimerState::EXPIRED,会返回DEFAULT。

604  Scheduler::RefreshRateType Scheduler::calculateRefreshRateType() {
......  
649      // As long as touch is active we want to be in performance mode
650      if (mCurrentTouchState == TouchState::ACTIVE) {
651          REFRESH_RATE_RESULT(RESULT_TOUCH_ACTIVE);
652          return RefreshRateType::PERFORMANCE;
653      }
......  
655      // If timer has expired as it means there is no new content on the screen
656      if (mCurrentIdleTimerState == IdleTimerState::EXPIRED) {
657          REFRESH_RATE_RESULT(RESULT_IDLE_EXPIRED);
658          return RefreshRateType::DEFAULT;
659      }
......
707      return currRefreshRateType;
708  }

       策略函数给出的刷新率,会在handleTimerStateChanged中调用changeRefreshRate callback到SurfaceFlinger中,通过setRefreshRateTo接口往下传递,后面的调用关系,就基本跟setAllowedDisplayConfigs一致了。

       综上所诉,Google的刷新率策略如我所说,就是让你动手的时候,充分体验到高刷新率屏幕的硬件优势。至于其他情况,比如,有个应用界面,上面有个按钮,你点击这个按钮后,会播放一段动画。在你点击按钮的时候,Google的刷新率策略触发屏幕刷新率被改成90Hz,那么你看按钮按下抬起这个效果是很顺畅的。但是如果你的动画开始显示第一帧的时间跟按钮抬起结束的时间之间的间隔大于Idletimer的500毫秒生命周期的话,对不起,刷新率又被策略改回60Hz了。虽然你很想让动画也享受到90Hz的极致体验,但是奈何掌权者不给啊。所在Google的自适应策略下,如果想在用户操作后保持90Hz,你需要不停的更新显示内容,且每次更新之间不能大于500毫秒。这就是为什么很多支持高刷新的产品会设计固定刷新率的模式让用户做选择,因为只有在固定高刷新率的情况下,才能满足用户永远丝滑感受。当然,固定在90Hz也会带来不确定的问题,比如功耗的提高,比如对那些针对60Hz做过精心调试的游戏是否有负面效果。

4. HAL层刷新率相关代码

       上面讲了Android系统中APP和frameworks中跟显示刷新率和帧率相关的代码和策略。因为代码比较common,大家都可以做参考。现在讲到HAL层,因为不同平台的实现方法会有不同,我们就只依据高通平台的HAL粗略提一下,不做展开。

       前面有提到SurfaceFlinger是调用setActiveConfig()到HidlHAL去设置硬件刷新率的。在高通的HAL层,是一级级调到DisplayBase的SetActiveConfig接口,然后调用DRM HAL接口SetDisplayAttributes和UpdateMixerAttributes(),
去更新全局变量current_mode_index_和标记update_mode_ = true。current_mode_index_是记录系统当前的刷新率索引,update_mode_为true表示需要更新刷新率。

656  DisplayError DisplayBase::SetActiveConfig(uint32_t index) {
......
673    error = hw_intf_->SetDisplayAttributes(index);
......  
678    return ReconfigureDisplay();
679  }

       在SetDisplayAttributes还要确认一下所选的刷新率是否跟当前显示硬件的属性匹配:

847  DisplayError HWDeviceDRM::SetDisplayAttributes(uint32_t index) {
......
875    for (uint32_t mode_index = 0; mode_index < connector_info_.modes.size(); mode_index++) {
876      if ((to_set.vdisplay == connector_info_.modes[mode_index].mode.vdisplay) &&
877          (to_set.hdisplay == connector_info_.modes[mode_index].mode.hdisplay) &&
878          (to_set.vrefresh == connector_info_.modes[mode_index].mode.vrefresh) &&
879          (current_bit_clk == connector_info_.modes[mode_index].bit_clk_rate) &&
880          (mode_flag & connector_info_.modes[mode_index].mode.flags)) {
881        index = mode_index;
882        break;
883      }
884    }
885  
886    current_mode_index_ = index;
887    PopulateHWPanelInfo();
888    UpdateMixerAttributes();
...... 
902    return kErrorNone;
903  }

2023  void HWDeviceDRM::UpdateMixerAttributes() {
2024    uint32_t index = current_mode_index_;
2025  
2026    mixer_attributes_.width = display_attributes_[index].x_pixels;
2027    mixer_attributes_.height = display_attributes_[index].y_pixels;
2028    mixer_attributes_.split_left = display_attributes_[index].is_device_split
2029                                       ? hw_panel_info_.split_info.left_split
2030                                       : mixer_attributes_.width;
2031    mixer_attributes_.split_type = kNoSplit;
......  
2043    update_mode_ = true;
2044  }

       最后向kernel层发命令是在每帧刷新的Validate函数调用的SetupAtomic()中。它发送DRM驱动的property set code 命令CRTC_SET_MODE 给kernel,来改变驱动的刷新率。HAL层还有自己的刷新率更新策略来应对显示设备的变化,具体可以跟踪first_cycle_ 、vrefresh_ 、panel_mode_changed_这几个变量的变化。

1393  DisplayError HWDeviceDRM::Validate(HWLayers *hw_layers) {
......
1398    SetupAtomic(hw_layers, true );
1399  
1400    int ret = drm_atomic_intf_->Validate();
......
1409    return err;
1410  }

1076  void HWDeviceDRM::SetupAtomic(HWLayers *hw_layers, bool validate) {
......
1337    // Set CRTC mode, only if display config changes
1338    if (first_cycle_ || vrefresh_ || update_mode_ || panel_mode_changed_) {
1339      drm_atomic_intf_->Perform(DRMOps::CRTC_SET_MODE, token_.crtc_id, &current_mode);
1340    }
......
1352  }

5. kernel中刷新率的设定

        高通平台的DRM驱动比较复杂,且一般不需要大的修改。我们这里只提一下不同刷新率在device tree里的设定。在panel driver bringup阶段,从panel的spec中找出所支持的刷新率,并从IC vendor支持那里拿到相应的timing参数以后,就可以在device tree里生成该panel的device tree。关键的几个值是“qcom,mdss-dsi-panel-framerate”、“qcom,mdss-dsi-timing-switch-command”、“qcom,mdss-dsi-timing-switch-command-state”。

qcom,mdss-dsi-display-timings {
    timing@0{
        // 60 FPS
        qcom,mdss-dsi-cmd-mode;
        qcom,mdss-dsi-panel-framerate = ;
        .......
        qcom,mdss-dsi-timing-switch-command = [.....];
        qcom,mdss-dsi-on-command = [......]; 
        qcom,mdss-dsi-off-command = [......];  
        qcom,mdss-dsi-timing-switch-command-state = "dsi_lp_mode";
         ......
    };

    timing@1{
        // 90 FPS
        qcom,mdss-dsi-cmd-mode;
        qcom,mdss-dsi-panel-framerate = ;
        .......
        qcom,mdss-dsi-timing-switch-command = [......];
        qcom,mdss-dsi-on-command = [......]; 
        qcom,mdss-dsi-off-command = [......];  
        qcom,mdss-dsi-timing-switch-command-state = "dsi_lp_mode";
         ......
    };
};

6. 综述

       以上,在Android系统中跟屏幕刷新率相关的关键代码就理完了。虽然设备硬件的更新换代,相信大家会越来越多关注这方面对Android软件开发的影响。这里抛砖引玉,希望在Android的持续迭代过程中,与大家共同进步。欢迎不吝指正。

      本文禁止转载,如有需求,请联系作者。

whoatenereyhow 原创文章 2获赞 0访问量 95 关注 私信 展开阅读全文
作者:whoatenereyhow


免责声明:

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

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

Android 10 (Android Q)中的屏幕刷新率(display refresh rate)切换方法和策略

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

下载Word文档

猜你喜欢

Android 10 (Android Q)中的屏幕刷新率(display refresh rate)切换方法和策略

本文禁止转载,如有需求,请联系作者。 1. 屏幕刷新率和应用的显示帧率首先请区分好屏幕刷新率(Panel Refresh Rate)和应用的帧率(APP Frame Rate)两个概念。屏幕刷新率是显示器(LCD/OLED等)的硬件性能参数
2022-06-06

编程热搜

  • 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第一次实验

目录