In addition to Weibo, there is also WeChat
Please pay attention
WeChat public account
Shulou
2025-01-28 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Development >
Share
Shulou(Shulou.com)06/03 Report--
This article focuses on "how to monitor the FPS of an application". Interested friends may wish to have a look at it. The method introduced in this paper is simple, fast and practical. Let's let the editor take you to learn how to monitor the FPS of an application.
What is FPS?
Even if you don't know FPS, you must have heard the saying that in Android, the drawing time of each frame should not exceed 16.67ms. So, where did this 16.67ms come from? Is decided by FPS.
FPS,Frame Per Second, the number of frames displayed per second, also known as the frame rate. The FPS of an Android device is typically 60, which means 60 frames per second, so there is only a maximum of 1000 FPS 60 = 16.67ms left for each frame. Once the drawing time of a frame exceeds the limit, the frame will be dropped, and the user will see the same picture in two consecutive frames.
Monitoring FPS can reflect the stutter of the application to some extent, and the principle is also very simple, but only if you are familiar with the screen refresh mechanism and the drawing process. So I'm not going to go straight to the topic, let's start with View.invalidate ().
Start with View.invalidate ()
To explore the screen refresh mechanism and the View drawing process, View.invalidate () is undoubtedly a good choice, it will initiate a drawing process.
> View.java
Public void invalidate () {
Invalidate (true)
}
Public void invalidate (boolean invalidateCache) {
InvalidateInternal (0,0, mRight-mLeft, mBottom-mTop, invalidateCache, true)
}
Void invalidateInternal (int l, int t, int r, int b, boolean invalidateCache)
Boolean fullInvalidate) {
.
Final AttachInfo ai = mAttachInfo
Final ViewParent p = mParent
If (p! = null & & ai! = null & & l
< r && t < b) { final Rect damage = ai.mTmpInvalRect; damage.set(l, t, r, b); // 调用 ViewGroup.invalidateChild() p.invalidateChild(this, damage); } ...... } 这里调用到 ViewGroup.invalidateChild() 。 >ViewGroup.java
Public final void invalidateChild (View child, final Rect dirty) {
Final AttachInfo attachInfo = mAttachInfo
.
ViewParent parent = this
If (attachInfo! = null) {
.
Do {
View view = null
If (parent instanceof View) {
View = (View) parent
}
.
Parent = parent.invalidateChildInParent (location, dirty)
.
} while (parent! = null)
}
}
There is a recursion that keeps calling the invalidateChildInParent () method of the parent View up to the topmost parent View. It is easy to understand that View alone cannot draw its own, and must rely on the top-level parent View to measure, layout, and draw the entire View tree. But who is the parent View at the top level? Is it a layout file passed in by setContentView ()? No, it is parsed and stuffed into the DecorView. Is that DecorView? No, it has a father, too.
Who is the parent of DecorView? This brings you to the ActivityThread.handleResume () method.
> ActivityThread.java
Public void handleResumeActivity (IBinder token, boolean finalStateRequest, boolean isForward, String reason) {
.
/ / 1. Callback onResume ()
Final ActivityClientRecord r = performResumeActivity (token, finalStateRequest, reason)
.
View decor = r.window.getDecorView ()
Decor.setVisibility (View.INVISIBLE)
ViewManager wm = a.getWindowManager ()
/ / 2. Add decorView to WindowManager
Wm.addView (decor, l)
.
}
In the second step, the WindowManagerImpl.addView () method is actually called, and the WindowManagerGlobal.addView () method is called in WindowManagerImpl.
> WindowManagerGlobal.java
/ / Parameter view is DecorView
Public void addView (View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) {
.
ViewRootImpl root
/ / 1. Initialize ViewRootImpl
Root = new ViewRootImpl (view.getContext (), display)
MViews.add (view)
MRoots.add (root)
/ / 2. That's the point.
Root.setView (view, wparams, panelParentView)
.
}
Follow up the ViewRootImpl.setView () method.
> ViewRootImpl.java
/ / Parameter view is DecorView
Public void setView (View view, WindowManager.LayoutParams attrs, View panelParentView) {
Synchronized (this) {
If (mView = = null) {
MView = view
/ / 1. Initiate the first drawing
RequestLayout ()
/ / 2. Binder calls Session.addToDisplay () to add window to the screen
Res = mWindowSession.addToDisplay (mWindow, mSeq, mWindowAttributes
GetHostVisibility (), mDisplay.getDisplayId (), mWinFrame
MAttachInfo.mContentInsets, mAttachInfo.mStableInsets
MAttachInfo.mOutsets, mAttachInfo.mDisplayCutout, mInputChannel)
/ / 3. The point is, notice that view is DecorView,this and ViewRootImpl itself.
View.assignParent (this)
}
}
}
Follow up the View.assignParent () method.
> View.java
/ / Parameter parent is ViewRootImpl
Void assignParent (ViewParent parent) {
If (mParent = = null) {
MParent = parent
} else if (parent = = null) {
MParent = null
} else {
Throw new RuntimeException ("view" + this + "being added, but"
+ "it already has a parent")
}
}
Remember what we've been doing for so long? To explore the refresh process of View, we follow the View.invalidate () method all the way to ViewGroup.invalidateChild (), where the invalidateChildInParent () method of parent is called recursively. So we're looking for DecorView's dad. It is now clear that DecorView's father is ViewRootImpl, so the final call is the ViewRootImpl.invalidateChildInParent () method.
> ViewRootImpl.java
Public ViewParent invalidateChildInParent (int [] location, Rect dirty) {
/ / 1. Thread check
CheckThread ()
If (dirty = = null) {
/ / 2. Call scheduleTraversals ()
Invalidate ()
Return null
} else if (dirty.isEmpty () & &! mIsAnimating) {
Return null
}
.
/ / 3. Call scheduleTraversals ()
InvalidateRectOnScreen (dirty)
Return null
}
Whether it's invalite () at comment 2 or invalidateRectOnScreen () at comment 3, you end up calling the scheduleTraversals () method.
ScheduleTraversals () is an extremely important method in the View drawing process, and I have to have a separate section to talk about it.
The "choreographer" that connects the preceding and the following
In the previous section, we started with the View.invalidate () method and followed it all the way to the ViewRootImpl.scheduleTraversals () method.
> ViewRootImpl.java
Void scheduleTraversals () {
/ / 1. Prevent repeated calls
If (! mTraversalScheduled) {
MTraversalScheduled = true
/ / 2. Send a synchronization barrier to ensure priority for asynchronous messages
MTraversalBarrier = mHandler.getLooper (). GetQueue (). PostSyncBarrier ()
/ / 3. Will eventually perform the task of mTraversalRunnable.
MChoreographer.postCallback (
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null)
.
}
}
MTraversalScheduled is a Boolean value that prevents repeated calls. Multiple calls during a vsync signal are meaningless, using Handler's synchronization barrier mechanism to give priority to asynchronous message Choreographer debut.
At this point, it's time for the famous choreographer Choreographer (to avoid the embarrassment of not being able to read words in the interview, mastering the pronunciation is still necessary).
A task mTraversalRunnable is sent through mChoreographer and will eventually be executed at some point. Before looking at the source code, throw out a few questions:
When was mChoreographer initialized? What the heck is mTraversalRunnable? How does mChoreographer send tasks and how are tasks scheduled for execution?
Around these three questions, let's go back to the source code.
Let's take a look at the first question, which goes back to the WindowManagerGlobal.addView () method introduced in the previous section.
> WindowManagerGlobal.java
/ / Parameter view is DecorView
Public void addView (View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) {
.
ViewRootImpl root
/ / 1. Initialize ViewRootImpl
Root = new ViewRootImpl (view.getContext (), display)
MViews.add (view)
MRoots.add (root)
Root.setView (view, wparams, panelParentView)
.
}
A new ViewRootImpl object is created at note 1 to follow up the constructor of ViewRootImpl.
> ViewRootImpl.java
Public ViewRootImpl (Context context, Display display) {
MContext = context
/ / 1. IWindowSession proxy object for Binder communication with WMS
MWindowSession = WindowManagerGlobal.getWindowSession ()
.
MThread = Thread.currentThread ()
.
/ / IWindow Binder object
MWindow = new W (this)
.
/ / 2. Initialize mAttachInfo
MAttachInfo = new View.AttachInfo (mWindowSession, mWindow, display, this, mHandler, this
Context)
.
/ / 3. Initialize Choreographer and store through Threadlocal
MChoreographer = Choreographer.getInstance ()
.
}
In the constructor of ViewRootImpl, mChoreographer is initialized at note 3, and the Choreographer.getInstance () method is called.
> Choreographer.java
Public static Choreographer getInstance () {
Return sThreadInstance.get ()
}
SThreadInstance is a ThreadLocal object.
> Choreographer.java
Private static final ThreadLocal sThreadInstance =
New ThreadLocal () {
@ Override
Protected Choreographer initialValue () {
Looper looper = Looper.myLooper ()
If (looper = = null) {
Throw new IllegalStateException ("The current thread must have a looper!")
}
/ / create a new Choreographer object
Choreographer choreographer = new Choreographer (looper, VSYNC_SOURCE_APP)
If (looper = = Looper.getMainLooper ()) {
MMainInstance = choreographer
}
Return choreographer
}
}
So mChoreographer saves the thread private object in ThreadLocal. Its constructor needs to pass in the Looper object of the current thread (in this case, the main thread).
Another digression here, when was the main thread Looper created? Review the process of creating the application process:
Call Process.start () to create an application process
ZygoteProcess is responsible for establishing a socket connection with the Zygote process and sending the parameters needed by the creation process to the socket server of Zygote
After receiving the parameters, the Zygote server calls ZygoteConnection.processOneCommand () to process the parameters and fork the process.
Finally, the main () method of the ActivityThread class is found through findStaticMain () and executed, and the child process is started.
ActivityThread is not a thread, but it runs on the main thread, and the main thread Looper is executed in its main () method.
> ActivityThread.java
Public static void main (String [] args) {
.
/ / create the main thread Looper
Looper.prepareMainLooper ()
.
/ / create ActivityThread and attach (false)
ActivityThread thread = new ActivityThread ()
Thread.attach (false, startSeq)
.
/ / start the main thread message loop
Looper.loop ()
}
Looper is also stored in ThreadLocal.
Going back to Choreographer, let's take a look at its constructor.
> Choreographer.java
Private Choreographer (Looper looper, int vsyncSource) {
MLooper = looper
/ / handling events
MHandler = new FrameHandler (looper)
/ / USE_VSYNC defaults to true after Android 4.1,
/ / FrameDisplayEventReceiver is a vsync event receiver
MDisplayEventReceiver = USE_VSYNC
? New FrameDisplayEventReceiver (looper, vsyncSource)
: null
MLastFrameTimeNanos = Long.MIN_VALUE
/ / the time of a frame. 60pfs is 16.7ms.
MFrameIntervalNanos = (long) (1000000000 / getRefreshRate ())
/ / callback queue
MCallbackQueues = new callback queue [CALLBACK _ LAST + 1]
For (int I = 0; I ViewRootImpl.java
Final TraversalRunnable mTraversalRunnable = new TraversalRunnable ()
Final class TraversalRunnable implements Runnable {
@ Override
Public void run () {
DoTraversal ()
}
}
Nothing special, it's just a Runnable object, and the doTraversal () method is executed in the run () method.
> ViewRootImpl.java
Void doTraversal () {
If (mTraversalScheduled) {
/ / 1. Set mTraversalScheduled to false
MTraversalScheduled = false
/ / 2. Remove the synchronization barrier
MHandler.getLooper (). GetQueue (). RemoveSyncBarrier (mTraversalBarrier)
/ / 3. Start the layout, measurement, and drawing process
PerformTraversals ()
.
}
Compare again with the scheduleTraversals () method that initially initiated the drawing:
> ViewRootImpl.java
Void scheduleTraversals () {
/ / 1. Set mTraversalScheduled to true to prevent repeated calls
If (! mTraversalScheduled) {
MTraversalScheduled = true
/ / 2. Send a synchronization barrier to ensure priority for asynchronous messages
MTraversalBarrier = mHandler.getLooper (). GetQueue (). PostSyncBarrier ()
/ / 3. Will eventually perform the task of mTraversalRunnable.
MChoreographer.postCallback (
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null)
.
}
}
If you take a closer look at the notes 1, 2, and 3 of the above two methods, it is still very clear. After the mTraversalRunnable is executed, the performTraversals () method is eventually called to complete the entire View measurement, layout and drawing process.
At this point in the analysis, the last step is, how is mTraversalRunnable scheduled for execution? Let's go back to the Choreographer.postCallback () method.
> Choreographer.java
Public void postCallback (int callbackType, Runnable action, Object token) {
PostCallbackDelayed (callbackType, action, token, 0)
}
Public void postCallbackDelayed (int callbackType
Runnable action, Object token, long delayMillis) {
.
PostCallbackDelayedInternal (callbackType, action, token, delayMillis)
}
/ / the parameters passed are Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, and null,0 in turn
Private void postCallbackDelayedInternal (int callbackType
Object action, Object token, long delayMillis) {
.
Synchronized (mLock) {
Final long now = SystemClock.uptimeMillis ()
Final long dueTime = now + delayMillis
/ / 1. Cram mTraversalRunnable into the queue
MCallbackQueues [callbackType] .addCallbackLocked (dueTime, action, token)
If (dueTime Choreographer.java
Private void scheduleFrameLocked (long now) {
If (! mFrameScheduled) {
MFrameScheduled = true
If (USE_VSYNC) {/ / Android 4.1After USE_VSYNCUSE_VSYNC defaults to true
/ / if it is the current thread, apply for vsync directly, otherwise communicate through handler
If (isRunningOnLooperThreadLocked ()) {
ScheduleVsyncLocked ()
} else {
/ / send asynchronous messages
Message msg = mHandler.obtainMessage (MSG_DO_SCHEDULE_VSYNC)
Msg.setAsynchronous (true)
MHandler.sendMessageAtFrontOfQueue (msg)
}
} else {/ / when vsync,4.1 is not enabled, it is enabled by default
Final long nextFrameTime = Math.max (
MLastFrameTimeNanos / TimeUtils.NANOS_PER_MS + sFrameDelay, now)
Message msg = mHandler.obtainMessage (MSG_DO_FRAME)
Msg.setAsynchronous (true)
MHandler.sendMessageAtTime (msg, nextFrameTime)
}
}
}
Since the beginning, VSYNC has been mentioned several times, and there is also a video introduction to Android Performance Patterns: Understanding VSYNC [14], which you can take a look at. In short, VSYNC is to solve the problem of "screen tearing" caused by the inconsistency between the screen refresh rate and the GPU frame rate. VSYNC has existed for a long time on the PC side, but it was introduced into the Android display system by Google after 4.1in order to solve the problem that UI display is not smooth.
To put it simply, VSYNC can be regarded as a timing signal sent by hardware, and the signal is monitored by Choreographer. Whenever the signal comes, the unity begins to draw. This is what the scheduleVsyncLocked () method does.
> Choreographer.java
Private void scheduleVsyncLocked () {
MDisplayEventReceiver.scheduleVsync ()
}
MDisplayEventReceiver is a FrameDisplayEventReceiver object, but it does not have a scheduleVsync () method, but a parent class method that is called directly. The parent class of FrameDisplayEventReceiver is DisplayEventReceiver.
> DisplayEventReceiver.java
Public abstract class DisplayEventReceiver {
Public void scheduleVsync () {
If (mReceiverPtr = = 0) {
Log.w (TAG, "Attempted to schedule a vertical sync pulse but the display event"
+ "receiver has already been disposed.")
} else {
/ / registers to listen for vsync signals and calls back the dispatchVsync () method
NativeScheduleVsync (mReceiverPtr)
}
}
/ / this method is called by native when there is a vsync signal
Private void dispatchVsync (long timestampNanos, int builtInDisplayId, int frame) {
/ / timestampNanos is the time of vsync callback
OnVsync (timestampNanos, builtInDisplayId, frame)
}
Public void onVsync (long timestampNanos, int builtInDisplayId, int frame) {
}
}
In the scheduleVsync () method, the next listening of the vsync signal will be registered through the nativeScheduleVsync () method. As can be seen from the method name, the following native call will be entered, and the level is limited, so it will not be pursued.
After registering listening, the next time the vsync signal comes, the dispatchVsync () method of the java layer is called back through jni, where the onVsync () method is called. The onVsync () method of the parent class DisplayEventReceiver is an empty implementation, so let's go back to the subclass FrameDisplayEventReceiver, which is the inner class of Choreographer.
> Choreographer.java
Private final class FrameDisplayEventReceiver extends DisplayEventReceiver
Implements Runnable {
Private long mTimestampNanos
Private int mFrame
/ / vsync signal monitoring callback
@ Override
Public void onVsync (long timestampNanos, int builtInDisplayId, int frame) {
.
Long now = System.nanoTime ()
/ timestampNanos is the time of vsync callback, which cannot be greater than now
If (timestampNanos > now) {
Log.w (TAG, "Frame time is" + ((timestampNanos-now) * 0.000001f)
+ "ms in the future! Check that graphics HAL is generating vsync"
+ "timestamps using the correct timebase.")
TimestampNanos = now
}
.
MTimestampNanos = timestampNanos
MFrame = frame
/ / the this passed in here will call back its own run () method
Message msg = Message.obtain (mHandler, this)
/ / this is an asynchronous message, which ensures priority execution.
Msg.setAsynchronous (true)
MHandler.sendMessageAtTime (msg, timestampNanos / TimeUtils.NANOS_PER_MS)
}
@ Override
Public void run () {
DoFrame (mTimestampNanos, mFrame)
}
}
In the onVsync () callback, an asynchronous message is sent to the main thread. Notice that the time in the sendMessageAtTime () method parameter is timestampNanos / TimeUtils. TimestampNanos is the timestamp of the vsync signal in nanoseconds, so here we do a division and convert it to milliseconds. By the time the code is executed here, the vsync signal has already occurred, so the timestampNanos is smaller than the current time. In this way, when the message is stuffed into the MessageQueue, it can be stuffed directly to the front. In addition, callback is this, so when the message is executed, it calls its own run () method, and the run () method calls the doFrame () method.
> Choreographer.java
Void doFrame (long frameTimeNanos, int frame) {
Final long startNanos
Synchronized (mLock) {
If (! mFrameScheduled) {
Return; / / no work to do
}
.
Long intendedFrameTimeNanos = frameTimeNanos
StartNanos = System.nanoTime ()
/ / calculate the timeout
/ / frameTimeNanos is the time of callback of vsync signal, and startNanos is the current timestamp
/ / subtract the time it takes to get the main thread
Final long jitterNanos = startNanos-frameTimeNanos
/ / mFrameIntervalNanos is the time of one frame
If (jitterNanos > = mFrameIntervalNanos) {
Final long skippedFrames = jitterNanos / mFrameIntervalNanos
/ / drop more than 30 frames and print log
If (skippedFrames > = SKIPPED_FRAME_WARNING_LIMIT) {
Log.i (TAG, "Skipped" + skippedFrames + "frames!"
+ "The application may be doing too much work on its main thread.")
}
Final long lastFrameOffset = jitterNanos% mFrameIntervalNanos
FrameTimeNanos = startNanos-lastFrameOffset
}
.
}
Try {
AnimationUtils.lockAnimationClock (frameTimeNanos / TimeUtils.NANOS_PER_MS)
/ / doCallBacks () starts the callback
MFrameInfo.markInputHandlingStart ()
DoCallbacks (Choreographer.CALLBACK_INPUT, frameTimeNanos)
MFrameInfo.markAnimationsStart ()
DoCallbacks (Choreographer.CALLBACK_ANIMATION, frameTimeNanos)
MFrameInfo.markPerformTraversalsStart ()
DoCallbacks (Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos)
DoCallbacks (Choreographer.CALLBACK_COMMIT, frameTimeNanos)
} finally {
AnimationUtils.unlockAnimationClock ()
}
.
}
The mTraversalRunnable is stuffed into the mCallbackQueues [] array in the Choreographer.postCallback () method, and the following doCallbacks () method is about to take it out and execute it.
> Choreographer.java
Void doCallbacks (int callbackType, long frameTimeNanos) {
CallbackRecord callbacks
Synchronized (mLock) {
Final long now = System.nanoTime ()
/ / find the corresponding CallbackRecord object according to callbackType
Callbacks = mCallbackQueues [callbackType] .extractDueCallbacksLocked (
Now / TimeUtils.NANOS_PER_MS)
If (callbacks = = null) {
Return
}
MCallbacksRunning = true
.
}
Try {
For (CallbackRecord c = callbacks; c! = null; c = c.next) {
/ / execute callBack
C.run (frameTimeNanos)
}
} finally {
.
}
}
Find the corresponding mCallbackQueues according to callbackType, and then execute it, and the specific process will not be analyzed in depth. There are four types of callbackType, which are CALLBACK_INPUT, CALLBACK_ANIMATION, CALLBACK_TRAVERSAL and CALLBACK_COMMIT.
> Choreographer.CallbackRecord
Public void run (long frameTimeNanos) {
If (token = = FRAME_CALLBACK_TOKEN) {
((FrameCallback) action) .doFrame (frameTimeNanos)
} else {
(Runnable) action) .run ()
}
}
At this point, the mTraversalRunnable is executed and the entire process of View.invalidate () goes through. To sum up:
Starting with View.invalidate (), the parent.invalidateChildInParent () method is called recursively. The top parent here is ViewRootImpl. ViewRootImpl is the parent of DecorView, and the chain of assignment calls is ActivityThread.handleResumeActivity-> WindowManagerImpl.addView ()-> WindowManagerGlobal.addView ()-> ViewRootImpl.setView ()-> View.assignParent ().
ViewRootImpl.invalidateChildInParent () finally calls the scheduleTraversals () method, where after setting up the synchronization barrier, the task mTraversalRunnable is submitted through the Choreographer.postCallback () method, which is responsible for measuring, laying out, and drawing the View.
The Choreographer.postCallback () method registers the next monitoring of the vsync signal to the underlying layer of the system through the DisplayEventReceiver.nativeScheduleVsync () method. When the next vsync comes, the system will call back its dispatchVsync () method and finally its FrameDisplayEventReceiver.onVsync () method.
The previously submitted mTraversalRunnable is taken out in the FrameDisplayEventReceiver.onVsync () method and executed. In this way, the whole drawing process is completed.
How to monitor the FPS of the application?
Monitoring the current application of FPS is simple. In each vsync signal callback, callback tasks in four types of mCallbackQueues queues are performed. On the other hand, Choreographer provides a method to submit callback tasks, which is Choreographer.getInstance (). PostFrameCallback (). Simply go in and have a look.
> Choreographer.java
Public void postFrameCallback (FrameCallback callback) {
PostFrameCallbackDelayed (callback, 0)
}
Public void postFrameCallbackDelayed (FrameCallback callback, long delayMillis) {
.
/ / the type here is CALLBACK_ANIMATION
PostCallbackDelayedInternal (CALLBACK_ANIMATION
Callback, FRAME_CALLBACK_TOKEN, delayMillis)
}
It is basically the same as the Choreographer.postCallback () called in the View.invalite () process, except that the callback type is not consistent, in this case CALLBACK_ANIMATION.
I directly give the implementation code: FpsMonitor.kt [15]
Object FpsMonitor {
Private const val FPS_INTERVAL_TIME = 1000L
Private var count = 0
Private var isFpsOpen = false
Privateval fpsRunnable by lazy {FpsRunnable ()}
Privateval mainHandler by lazy {Handler (Looper.getMainLooper ())}
Privateval listeners = arrayListOf Unit > ()
Fun startMonitor (listener: (Int)-> Unit) {
/ / prevent repeated opening
If (! isFpsOpen) {
IsFpsOpen = true
Listeners.add (listener)
MainHandler.postDelayed (fpsRunnable, FPS_INTERVAL_TIME)
Choreographer.getInstance () postFrameCallback (fpsRunnable)
}
}
Fun stopMonitor () {
Count = 0
MainHandler.removeCallbacks (fpsRunnable)
Choreographer.getInstance () removeFrameCallback (fpsRunnable)
IsFpsOpen = false
}
Class FpsRunnable: Choreographer.FrameCallback, Runnable {
Override fun doFrame (frameTimeNanos: Long) {
Count++
Choreographer.getInstance () postFrameCallback (this)
}
Override fun run () {
Listeners.forEach {it.invoke (count)}
Count = 0
MainHandler.postDelayed (this, FPS_INTERVAL_TIME)
}
}
}
The general logic goes like this:
The declaration variable count is used to count the number of callbacks to monitor the next vsync signal through Choreographer.getInstance (). PostFrameCallback (fpsRunnable) registration, submit the task, the task callback only does two things, one is count++, the other is to continue to register to monitor the next vsync signal. Do a scheduled task through Handler, count the count value every other second and clear it. At this point, I believe you have a deeper understanding of "how to monitor the FPS of the application". You might as well do it in practice. Here is the website, more related content can enter the relevant channels to inquire, follow us, continue to learn!
Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.
Views: 0
*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.
Continue with the installation of the previous hadoop.First, install zookooper1. Decompress zookoope
"Every 5-10 years, there's a rare product, a really special, very unusual product that's the most un
© 2024 shulou.com SLNews company. All rights reserved.