The Mechanics of the View Drawing Process
Every button, text block, and image rendered on the screen undergoes the exact same architectural pipeline: Measure → Layout → Draw. Comprehending the source code logic dictating these three phases is the absolute prerequisite for mastering Custom View engineering and UI performance optimization.
The Origin of Rendering and the Propulsion Mechanism
All drawing operations ultimately converge at ViewRootImpl.performTraversals()—the master dispatcher for rendering the entire View hierarchy. But how is this dispatcher triggered? And how does subsequent rendering execute continuously? This relies on a highly precise message scheduling and VSync (Vertical Synchronization) mechanism.
How is the First Drawing Message Dispatched?
After an Activity is launched, the initial request to render the View hierarchy onto the screen originates directly after the onResume() lifecycle callback.
Think of this as a newly opened restaurant: The kitchen (ViewRootImpl) receives its very first order (the display window) and begins preparing the dish (rendering).
- The Entry Point:
addViewWithinActivityThread.handleResumeActivity, after the Activity lifecycle reachesonResume, the system invokesWindowManager.addView()to inject the DecorView into the Window. - Establishing the Link:
ViewRootImpl.setViewInside this method, the system instantiates theViewRootImpl. It assumes responsibility for managing the interaction between the entire View tree and the OS, invokingsetView()to prepare for display. - Requesting Layout:
requestLayoutsetView()internally invokesrequestLayout(), flagging the system that a comprehensive measurement and layout pass is required. - Core Scheduling:
scheduleTraversalsrequestLayout()immediately triggersscheduleTraversals(), the most critical preparatory step before rendering. It executes two vital operations:- Deploying a Sync Barrier: It injects a specialized "VIP interception barricade" into the main thread's
MessageQueue. This barrier blocks all standard synchronous messages, guaranteeing that the impending rendering task (an asynchronous message) holds supreme execution priority. - Registering the Choreographer Callback: It delegates the actual rendering payload (
mTraversalRunnable) to the system's "orchestrator," theChoreographer, and requests the next VSync signal from the underlying hardware layer.
- Deploying a Sync Barrier: It injects a specialized "VIP interception barricade" into the main thread's
The source code explicitly maps out this vector:
// ViewRootImpl.java
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
// 1. Deploy the Sync Barrier: prioritize UI rendering by blocking standard sync messages
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
// 2. Submit a CALLBACK_TRAVERSAL task to the Choreographer
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
}
}
The architectural trace of the initial rendering request:
sequenceDiagram
participant AT as ActivityThread
participant WM as WindowManager
participant VR as ViewRootImpl
participant MQ as MessageQueue
participant CH as Choreographer
AT->>WM: addView(DecorView)
WM->>VR: setView()
VR->>VR: requestLayout()
VR->>VR: scheduleTraversals()
VR->>MQ: postSyncBarrier() (Deploy Sync Barrier)
VR->>CH: postCallback(TRAVERSAL)
Note over CH: Awaiting the next VSync signal
When the underlying hardware emits the next VSync signal, the Choreographer is notified, triggering the very first performTraversals().
How Does the Drawing Loop Sustain Itself?
Post-initial render, the App enters the user interaction phase. The continuous loop of rendering messages does not generate spontaneously; it is driven jointly by UI update demands + VSync signals.
Think of a Train Station Dispatch System:
The VSync signal represents the train schedule (e.g., departing every 16.6ms for a 60Hz display). Re-draw requests from the app (
invalidateorrequestLayout) represent passengers buying tickets. The train only departs if there are ticketed passengers AND the scheduled departure time has arrived. If no one buys a ticket, the train remains stationary at departure time (conserving resources).
- Generating the Re-draw Demand (Buying the Ticket)
When code invokes
invalidate(),requestLayout(), or a property animation is executing, the request propagates up the View tree, ultimately invokingViewRootImpl.scheduleTraversals()once again. - Registering the Callback and Awaiting Departure
scheduleTraversals()re-registers a drawing payload (CALLBACK_TRAVERSAL) with theChoreographerand invokesscheduleVsync()to request the next VSync signal from the OS. - VSync Signal Arrival (Departure Time)
SurfaceFlinger (the system service governing graphic compositing) generates the hardware VSync signal, transmitting it via Binder to the application layer's
DisplayEventReceiver.onVsync(). - Dispatching the Privileged Message
Upon receiving the signal,
onVsync()posts an asynchronous message (MSG_DO_FRAME) to the main thread. BecausescheduleTraversalspreviously erected a Sync Barrier, the main thread bypasses all queued standard messages to execute this privileged message instantly. - Executing doFrame (The Train Departs)
The main thread executes
Choreographer.doFrame(). Functioning as the conductor, this method strictly executes tasks based on their timestamps across distinct phases:- Process Input Events (INPUT)
- Execute Animation Calculus (ANIMATION)
- Execute View Traversal (TRAVERSAL, invoking
mTraversalRunnablewhich triggersperformTraversals, thereby executing Measure / Layout / Draw).
- Sustaining the Loop (The Secret of Continuous Animation)
- If the user simply tapped a button to alter its color, and no further redraws are requested post-render, the app ignores the subsequent VSync signal and sleeps (saving battery).
- If a continuous animation is running, the animation engine calculates property deltas during the
ANIMATIONphase and instantly firesinvalidate()before the frame concludes. This mimics a passenger "buying a ticket for the next train the moment they step off." Consequently, the next VSync signal triggersdoFrameagain, engineering a continuous, hyper-smooth rendering loop.
The VSync-driven rendering loop topology:
sequenceDiagram
participant UI as View (UI Layer)
participant VR as ViewRootImpl
participant CH as Choreographer
participant SF as SurfaceFlinger (Hardware)
participant MQ as MessageQueue
UI->>VR: invalidate() / requestLayout()
VR->>CH: postCallback(TRAVERSAL)
CH->>SF: scheduleVsync() (Request Departure)
Note over SF: 16.6ms later, hardware generates VSync
SF-->>CH: onVsync() (Signal Arrival)
CH->>MQ: sendMessage(MSG_DO_FRAME) (Async Message, bypasses Barrier)
MQ-->>CH: Executes doFrame()
CH->>CH: 1. doCallbacks(INPUT)
CH->>CH: 2. doCallbacks(ANIMATION)
CH->>CH: 3. doCallbacks(TRAVERSAL)
CH->>VR: Executes mTraversalRunnable
VR->>VR: performTraversals()
The core implementation of doFrame and mTraversalRunnable:
// Choreographer.java
void doFrame(long frameTimeNanos, int frame) {
// Strictly execute callback phases in chronological order
doCallbacks(Choreographer.CALLBACK_INPUT, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_ANIMATION, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_INSETS_ANIMATION, frameTimeNanos);
doCallbacks(Choreographer.CALLBACK_TRAVERSAL, frameTimeNanos); // Triggers performTraversals
doCallbacks(Choreographer.CALLBACK_COMMIT, frameTimeNanos);
}
// ViewRootImpl.java
final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
// Purge the Sync Barrier, allowing blocked standard messages to resume
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
// Ignite the three fundamental rendering phases
performTraversals();
}
}
Through this "On-Demand Request + VSync Timing" architecture, Android perfectly synchronizes UI rendering with the display refresh rate, obliterating legacy screen tearing artifacts while preventing catastrophic CPU/GPU resource waste.
performTraversals: Executing the Three Pillars
When doFrame finally relinquishes control to performTraversals(), the actual drawing sequence detonates:
ViewRootImpl.performTraversals()
├── performMeasure(childWidthMeasureSpec, childHeightMeasureSpec)
│ └── mView.measure(widthSpec, heightSpec)
│ └── onMeasure(widthSpec, heightSpec)
│
├── performLayout(lp, desiredWindowWidth, desiredWindowHeight)
│ └── mView.layout(0, 0, mView.getMeasuredWidth(), mView.getMeasuredHeight())
│ └── onLayout(changed, l, t, r, b)
│
└── performDraw()
└── draw(fullRedrawNeeded)
└── drawSoftware(surface, ...)
└── mView.draw(canvas)
└── onDraw(canvas)
mView is the DecorView (the apex node of the Activity Window). The process executes a top-down recursive traversal spanning the entire View tree.
Phase I: Measure
Objective: Mathematically determine the width and height of every View (yielding measuredWidth and measuredHeight).
The measurement phase is architecturally identical to a corporate budget approval workflow.
ViewGroup(The Executive) is aware of its total spatial budget.View(The Subordinate) submits its requested dimensions viaLayoutParams(e.g., hardcoded 100dp,match_parent, orwrap_content).- The finalized dimensions must be negotiated between the "Executive's available budget" and the "Subordinate's requested requirements."
The Core Vector: MeasureSpec (The Budget Approval Document)
Within the source code, the "approval document" passed down is the MeasureSpec. For extreme performance optimization, Android engineers this as a single 32-bit int: The highest 2 bits encode the Measurement Mode, while the lower 30 bits encode the Size.
// MeasureSpec Bitwise Architecture
// |-- 2 bit mode --|------ 30 bit size ------|
// | EXACTLY(01) | 500px |
public static class MeasureSpec {
private static final int MODE_SHIFT = 30;
private static final int MODE_MASK = 0x3 << MODE_SHIFT;
// The Three Operational Modes
public static final int UNSPECIFIED = 0 << MODE_SHIFT;
public static final int EXACTLY = 1 << MODE_SHIFT;
public static final int AT_MOST = 2 << MODE_SHIFT;
public static int makeMeasureSpec(int size, int mode) {
return (size & ~MODE_MASK) | (mode & MODE_MASK);
}
public static int getMode(int measureSpec) {
return (measureSpec & MODE_MASK);
}
public static int getSize(int measureSpec) {
return (measureSpec & ~MODE_MASK);
}
}
| Measurement Mode | Semantic Definition | Common Triggers |
|---|---|---|
| EXACTLY | Absolute Mandate: "Your budget is precisely size. Do not deviate." | Concrete dp values applied, or match_parent (provided the parent is also EXACTLY). |
| AT_MOST | Upper Limit: "Your budget maxes out at size. Scale to fit within it." | wrap_content applied. |
| UNSPECIFIED | Infinite Budget: "Take as much as you need; no limits apply." | Vertical measurement by a ScrollView or ListView against its children (infinite scrolling potential). |
Architectural Interrogation: Where does the Apex View's MeasureSpec originate?
If a child View's MeasureSpec is synthesized by its parent, who issues the very first "budget" to the absolute apex node (the DecorView), which possesses no parent?
Answer: It is mathematically synthesized from the physical dimensions of the Window and the DecorView's own LayoutParams.
This "angel investment" is violently hardcoded by ViewRootImpl during performMeasure via the getRootMeasureSpec() function.
// ViewRootImpl.java
private static int getRootMeasureSpec(int windowSize, int rootDimension) {
int measureSpec;
switch (rootDimension) {
case ViewGroup.LayoutParams.MATCH_PARENT:
// Window size dictates the exact size of the Apex View
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.EXACTLY);
break;
case ViewGroup.LayoutParams.WRAP_CONTENT:
// Apex View dictates its own size, capped by the Window size
measureSpec = MeasureSpec.makeMeasureSpec(windowSize, MeasureSpec.AT_MOST);
break;
default:
// Apex View demands an exact physical dp size
measureSpec = MeasureSpec.makeMeasureSpec(rootDimension, MeasureSpec.EXACTLY);
break;
}
return measureSpec;
}
The physical boundaries of the Window permanently lock the initial constraints of the DecorView. Armed with this initial "budget," the DecorView cascades downwards, layer by layer, issuing budgets to subordinates via getChildMeasureSpec.
The Core Logic: getChildMeasureSpec (Budget Approval Rules)
How is a child View's MeasureSpec actually computed? The heavy lifting resides within a paramount method in ViewGroup: getChildMeasureSpec().
Its logic translates to: The parent evaluates its own constraints (Parent MeasureSpec) against the child's demands (Child LayoutParams) to mathematically synthesize the final constraint payload (Child MeasureSpec).
graph TD
A[Parent MeasureSpec] --> C(getChildMeasureSpec)
B[Child LayoutParams] --> C
C --> D[Child MeasureSpec]
This complex matrix of "approval rules" is encoded into this canonical table:
| Parent MeasureSpec | Child Request: Exact dp | Child Request: match_parent |
Child Request: wrap_content |
|---|---|---|---|
| EXACTLY (Mandate) | EXACTLY (Requested dp) |
EXACTLY (Parent Size) |
AT_MOST (Parent Size) |
| AT_MOST (Limit) | EXACTLY (Requested dp) |
AT_MOST (Parent Size) |
AT_MOST (Parent Size) |
| UNSPECIFIED (Infinite) | EXACTLY (Requested dp) |
UNSPECIFIED (0) |
UNSPECIFIED (0) |
Analyzing the source code implementation of this matrix:
// ViewGroup.java
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
int specMode = MeasureSpec.getMode(spec);
int specSize = MeasureSpec.getSize(spec);
int size = Math.max(0, specSize - padding); // Deduct parent padding
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
case MeasureSpec.EXACTLY: // Parent operates under strict mandate
if (childDimension >= 0) { // Child demands a specific size (e.g., 100dp)
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY; // Approved. Grant exact size.
} else if (childDimension == LayoutParams.MATCH_PARENT) {
resultSize = size;
resultMode = MeasureSpec.EXACTLY; // Grant ALL remaining parent space as a strict mandate.
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
resultSize = size;
resultMode = MeasureSpec.AT_MOST; // Child scales itself, but is capped by remaining parent space.
}
break;
// ... AT_MOST and UNSPECIFIED logic follows identical mapping against the table
}
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
The Edge-Case Interrogation: What happens if a Child demands more space than the Parent possesses?
This is a highly penetrating edge-case test. Assume the Parent holds only 500dp of usable width (Mode EXACTLY), but the XML explicitly forces the Child to android:layout_width="1000dp". What occurs at runtime?
Re-evaluating the getChildMeasureSpec source code:
case MeasureSpec.EXACTLY: // Parent restricted to exactly 500dp
if (childDimension >= 0) { // Child aggressively demands 1000dp
resultSize = childDimension; // The payload size directly inherits 1000dp!
resultMode = MeasureSpec.EXACTLY; // Mode is EXACTLY
}
The Answer: The Parent blindly approves the request!
The Parent synthesizes a size=1000, mode=EXACTLY MeasureSpec for the Child. The Child blissfully measures itself at 1000dp, and during the Layout phase, it is physically positioned as 1000dp wide.
Why does it appear visually constrained to the parent's boundaries on screen?
Because during the Phase III Draw cycle, ViewGroup defaults to clipChildren = true. The rendering pipeline forcefully deploys Canvas.clipRect() to physically sever any pixels bleeding beyond the parent's 500dp boundaries.
The Child structurally exists at 1000dp, but the rendering engine amputates the excess 500dp before it hits the glass. If you manually disable android:clipChildren="false" on the Parent, the Child will violently breach the parent bounds and render across the entire display.
The Measurement Execution Chain: measure -> onMeasure -> setMeasuredDimension
Once the Parent synthesizes the Child's MeasureSpec, it fires the Child's measure() method.
sequenceDiagram
participant P as ViewGroup (Parent)
participant V as View (Child)
P->>V: measure(widthMeasureSpec, heightMeasureSpec)
Note over V: measure() is final; it handles caching and optimizations
V->>V: onMeasure(widthSpec, heightSpec)
Note over V: Engineers override onMeasure for custom computation
V->>V: setMeasuredDimension(width, height)
Note over V: Mandatory invocation to commit the final results
V-->>P: Measurement Complete
measure(): This is afinalmethod withinView, tasked with aggressive cache optimization. If theMeasureSpecremains unchanged, it aborts re-computation. Engineers are forbidden from overriding this.onMeasure(): The physical calculus occurs here. The View ingests theMeasureSpecand computes its physical boundaries.setMeasuredDimension(): Upon completion, this method must be invoked to lock the results intomMeasuredWidthandmMeasuredHeight. Failure to call this triggers a fatalIllegalStateException.
Custom View Trap: Why does wrap_content fail by default?
When engineering a Custom View by directly subclassing View, failing to override onMeasure results in wrap_content behaving identically to match_parent (it violently consumes all parent space).
The Architectural Cause resides in View.java's default implementation:
// Default View.java implementation
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(
getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec)
);
}
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST: // CRITICAL FAILURE POINT
case MeasureSpec.EXACTLY:
result = specSize; // Both AT_MOST and EXACTLY blindly return the MAXIMUM parent limit (specSize)!
break;
}
return result;
}
The source exposes that when a Child deploys wrap_content, the Parent assigns an AT_MOST mode. However, the default View.getDefaultSize() logic maps AT_MOST exactly like EXACTLY—it instantly maximizes its footprint to the absolute limit dictated by the Parent (specSize). This is why wrap_content breaks.
The Engineering Fix: You must manually calculate the "desired dimensions" and explicitly enforce limits under AT_MOST mode.
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSize = MeasureSpec.getSize(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSize = MeasureSpec.getSize(heightMeasureSpec)
// 1. Compute the "Internal Demand" (e.g., text width, bitmap bounds)
val desiredWidth = calculateContentWidth() + paddingLeft + paddingRight
val desiredHeight = calculateContentHeight() + paddingTop + paddingBottom
// 2. Cross-reference demand against the imposed Mode
val width = when (widthMode) {
MeasureSpec.EXACTLY -> widthSize // Mandate: Obey the specific size
MeasureSpec.AT_MOST -> min(desiredWidth, widthSize) // Cap: Take desired size, but NEVER breach the spec limit
else -> desiredWidth // Infinite: Take whatever you want
}
val height = when (heightMode) {
MeasureSpec.EXACTLY -> heightSize
MeasureSpec.AT_MOST -> min(desiredHeight, heightSize)
else -> desiredHeight
}
// 3. Commit the resolved topology
setMeasuredDimension(width, height)
}
Recursive Measurement by ViewGroup
A ViewGroup is responsible not only for its own footprint but for actively commanding all subordinates to measure themselves:
// ViewGroup.java
protected void measureChildren(int widthMeasureSpec, int heightMeasureSpec) {
final int size = mChildrenCount;
final View[] children = mChildren;
for (int i = 0; i < size; ++i) {
final View child = children[i];
if ((child.mViewFlags & VISIBILITY_MASK) != GONE) {
// Sequentially trigger measurement on all subordinates
measureChild(child, widthMeasureSpec, heightMeasureSpec);
}
}
}
protected void measureChild(View child, int parentWidthMeasureSpec, int parentHeightMeasureSpec) {
final LayoutParams lp = child.getLayoutParams();
// Synthesize the Child MeasureSpec, explicitly stripping out Parent padding
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight, lp.width);
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom, lp.height);
// Dispatch the execution order
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
Phase II: Layout
Objective: Mathematically anchor each View's physical coordinates (left, top, right, bottom) within its Parent.
// View.java
public void layout(int l, int t, int r, int b) {
// Phase check: Mandate re-measurement if necessary
if (...needsMeasure...) {
onMeasure(mOldWidthMeasureSpec, mOldHeightMeasureSpec);
}
int oldL = mLeft, oldT = mTop, oldR = mRight, oldB = mBottom;
boolean changed = setFrame(l, t, r, b); // Hardcode the four vertex vectors
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) != 0) {
onLayout(changed, l, t, r, b); // Delegate to subclasses for internal topology
}
}
For a standard View, onLayout is empty (it has no children to organize). For a ViewGroup, it is mathematically required to override onLayout to physically position its children:
// Example: Custom Vertical Linear Topology
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
var currentTop = paddingTop
for (i in 0 until childCount) {
val child = getChildAt(i)
if (child.visibility == View.GONE) continue
val childWidth = child.measuredWidth
val childHeight = child.measuredHeight
val childLeft = paddingLeft
child.layout(childLeft, currentTop, childLeft + childWidth, currentTop + childHeight)
currentTop += childHeight // Stack the next View directly beneath
}
}
Architectural Delta: getWidth() vs getMeasuredWidth():
getMeasuredWidth(): Locked during the Measure Phase; originates directly fromsetMeasuredDimension().getWidth(): Locked during the Layout Phase; mathematically equalsmRight - mLeft.- In 99% of topologies, these values are identical. However, if you maliciously inject altered parameters into
layout(), they will diverge permanently.
Phase III: Draw
Objective: Rasterize the View's payload onto the OS Canvas.
View.draw()'s source code explicitly defines a rigid 6-step rendering pipeline:
// View.java draw() Method
public void draw(Canvas canvas) {
// Step 1: Rasterize the Background
drawBackground(canvas);
// Step 2: Preserve Canvas Layers (Required for fading edges, routinely skipped)
// Step 3: Rasterize Native Payload (The Custom View logic)
onDraw(canvas);
// Step 4: Dispatch Draw to Children (Crucial for ViewGroups)
dispatchDraw(canvas);
// Step 5: Rasterize Fading Edges (Routinely skipped)
// Step 6: Rasterize Foreground (Decorations, Scrollbars)
onDrawForeground(canvas);
}
Custom View engineering predominantly operates within onDraw():
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
// Rasterizing a hardware-accelerated radial gradient
val shader = RadialGradient(
centerX, centerY, radius,
intArrayOf(Color.RED, Color.BLUE),
null, Shader.TileMode.CLAMP
)
paint.shader = shader
canvas.drawCircle(centerX, centerY, radius, paint)
}
requestLayout vs invalidate
These two methods serve as the exclusive triggers for UI updates. Conflating them destroys application performance:
requestLayout() invalidate()
│ │
▼ ▼
Inject PFLAG_FORCE_LAYOUT Flag coordinates as 'dirty'
Propagate upward to ViewRootImpl Propagate upward to ViewRootImpl
│ │
▼ ▼
performTraversals() performTraversals()
├── performMeasure ✅ ├── performMeasure ❌ (Aborted)
├── performLayout ✅ ├── performLayout ❌ (Aborted)
└── performDraw ✅ └── performDraw ✅
| Invocation | Phases Executed | Engineering Vector | Thread Context |
|---|---|---|---|
requestLayout() |
Measure + Layout + Draw | Physical dimension or position mutations | Main Thread |
invalidate() |
Draw only | Visual payload mutations (Color, Text, Alpha, Animation frames) | Main Thread |
postInvalidate() |
Draw only | Triggering redraws from background worker threads | Any Thread |
Why View.post(Runnable) Safely Retrieves Dimensions
Invoking view.getWidth() directly within Activity.onCreate invariably yields 0 because the UI thread has not yet executed measure/layout. However, view.post { } succeeds:
// Inside onCreate
textView.post {
Log.d("TAG", "width = ${textView.width}") // Successfully retrieves accurate bounds
}
The underlying mechanical sequence:
- When a View is attached to the window,
post()injects the Runnable payload directly into the Main Thread'sHandlerqueue. - The core
performTraversals()execution (which executes measure/layout/draw) is also a message queued in that same Main Thread Handler. - Crucially,
view.post()injects its Runnable behind theperformTraversals()message in the execution queue. - Therefore, when the Runnable finally executes, the View has mathematically completed its measure and layout cycles.
If the View is entirely detached, post() caches the Runnable within an internal View.mRunQueue, deferring injection until dispatchAttachedToWindow() executes.
Hardware Acceleration vs Software Rendering
Since API 14, Android violently defaults to Hardware Acceleration:
| Attribute | Software Rendering | Hardware Acceleration |
|---|---|---|
| Execution Engine | CPU + Skia | GPU + OpenGL/Vulkan |
| Canvas Architecture | Canvas |
DisplayListCanvas (Records RenderNodes) |
invalidate() Payload |
Re-rasterizes the entire View | Merely replays the mutated DisplayList |
| Performance Profile | Flexible but highly latency-prone | Drastically superior for 95% of workloads |
Hardware Accelerated Execution Pipeline:
Canvasoperations withinonDraw()are NOT immediately rasterized; they are recorded into aRenderNode(a DisplayList).- The
RenderThread(an isolated OS thread) compiles theRenderNodeinto raw GPU command sequences. - The GPU executes the final rasterization.
This physically explains why invalidate() is radically faster under Hardware Acceleration—it simply triggers a replay of the modified DisplayList on the GPU, completely bypassing the CPU traversal of the entire View tree.
Custom View Performance Doctrines
- Never Allocate Objects in
onDraw()(e.g.,Paint,Path,Rect).onDrawexecutes at 60/120Hz. Local allocations trigger catastrophic Garbage Collection (Memory Churn), destroying frame rates. Pre-allocate in the constructor. - Eradicate Overdraw—Ruthlessly strip away overlapping, invisible background layers that force the GPU to render pixels the user never sees.
- Deploy
clipRect()aggressively—Force the Canvas to exclusively render coordinates within the visible viewport bounds. requestLayout()is astronomically more expensive thaninvalidate()—Deploy it only when physical topologies mutate.- Batch Property Mutations—If altering 5 visual properties simultaneously, do not invoke
invalidate()5 times. Alter the state, then invoke a singleinvalidate()at the end of the transaction.