Skip to content

Commit

Permalink
LayoutInflater: Opportunistically create views directly for performance
Browse files Browse the repository at this point in the history
When opening and closing activities in Settings, a significant amount of
CPU time is spent performing JNI calls, as reported by simpleperf:

0.39%     /system/framework/arm64/boot-framework.oat                                                                                                art_jni_trampoline

Reflection in LayoutInflater is responsible for a significant portion
of the time spent in the JNI trampoline:

6.08%     0.08%  /apex/com.android.art/javalib/arm64/boot.oat                                                                            art_jni_trampoline
       |
       -- art_jni_trampoline
          |
          |--12.38%-- java.lang.reflect.Constructor.newInstance
          |    |--0.09%-- [hit in function]
          |    |
          |    |--88.32%-- android.view.LayoutInflater.createView
          |    |    |
          |    |    |--83.39%-- com.android.internal.policy.PhoneLayoutInflater.onCreateView
          |    |    |           android.view.LayoutInflater.onCreateView
          |    |    |           android.view.LayoutInflater.onCreateView
          |    |    |           android.view.LayoutInflater.createViewFromTag
          |    |    |    |
          |    |    |    |--72.73%-- android.view.LayoutInflater.rInflate
          |    |    |    |    |
          |    |    |    |    |--57.90%-- android.view.LayoutInflater.rInflate
          |    |    |    |    |    |
          |    |    |    |    |    |--94.90%-- android.view.LayoutInflater.inflate
          |    |    |    |    |    |           android.view.LayoutInflater.inflate
          |    |    |    |    |    |    |--35.86%-- [hit in function]
          |    |    |    |    |    |    |
          |    |    |    |    |    |    |--58.15%-- androidx.preference.PreferenceGroupAdapter.onCreateViewHolder

Empirical testing of interacting with ~113 real-world apps reveals that
many of the most frequently-inflated views are framework classes:

  13486 android.widget.LinearLayout
   6930 android.widget.View
   6447 android.widget.FrameLayout
   5613 android.widget.ViewStub
   5608 androidx.constraintlayout.widget.ConstraintLayout
   4722 android.widget.TextView
   4431 com.google.android.material.textview.MaterialTextView
   3570 eu.faircode.email.FixedTextView
   3044 android.widget.ImageView
   2665 android.widget.RelativeLayout
   1694 android.widget.Space
    979 androidx.preference.internal.PreferenceImageView
    926 androidx.appcompat.view.menu.ActionMenuItemView
    884 androidx.appcompat.widget.AppCompatImageView
    855 slack.uikit.components.icon.SKIconView
    770 android.widget.ProgressBar
    743 com.fastaccess.ui.widgets.FontTextView
    541 androidx.recyclerview.widget.RecyclerView
    442 androidx.appcompat.widget.AppCompatTextView
    404 org.mariotaku.twidere.view.MediaPreviewImageView
    393 com.moez.QKSMS.common.widget.QkTextView
    382 android.widget.Button
    365 slack.widgets.core.textview.ClickableLinkTextView
    365 slack.uikit.components.avatar.SKAvatarView
    352 com.google.android.libraries.inputmethod.widgets.SoftKeyView
    351 com.android.launcher3.BubbleTextView
    315 slack.widgets.core.viewcontainer.SingleViewContainer
    315 slack.widgets.core.textview.MaxWidthTextView
    313 androidx.constraintlayout.widget.Barrier
    302 slack.app.ui.widgets.ReactionsLayout
    302 slack.app.ui.messages.widgets.MessageLayout
    302 slack.app.ui.messages.widgets.MessageHeader
    290 com.android.launcher3.views.DoubleShadowBubbleTextView
    285 com.android.internal.widget.CachingIconView
    265 android.widget.ImageButton
    262 androidx.constraintlayout.widget.Guideline
    249 org.thoughtcrime.securesms.components.emoji.EmojiTextView
    234 com.google.android.libraries.inputmethod.widgets.AutoSizeTextView
    232 com.android.internal.widget.RemeasuringLinearLayout
    228 android.view.ViewStub
    227 android.app.ViewStub
    226 android.webkit.ViewStub
    221 im.vector.app.core.ui.views.ShieldImageView
    219 androidx.constraintlayout.widget.Group
    214 androidx.coordinatorlayout.widget.CoordinatorLayout
    204 androidx.appcompat.widget.ContentFrameLayout

All framework classes seen:

  13486 android.widget.LinearLayout
   6930 android.widget.View
   6447 android.widget.FrameLayout
   5613 android.widget.ViewStub
   4722 android.widget.TextView
   3044 android.widget.ImageView
   2665 android.widget.RelativeLayout
   1694 android.widget.Space
    770 android.widget.ProgressBar
    382 android.widget.Button
    265 android.widget.ImageButton
    228 android.view.ViewStub
    227 android.app.ViewStub
    226 android.webkit.ViewStub
    145 android.widget.Switch
    117 android.widget.DateTimeView
     86 android.widget.Toolbar
     68 android.widget.HorizontalScrollView
     67 android.widget.ScrollView
     65 android.widget.NotificationHeaderView
     65 android.webkit.NotificationHeaderView
     65 android.view.NotificationHeaderView
     65 android.app.NotificationHeaderView
     63 android.webkit.View
     63 android.view.View
     62 android.app.View
     58 android.widget.ListView
     50 android.widget.QuickContactBadge
     40 android.widget.SeekBar
     38 android.widget.CheckBox
     16 android.widget.GridLayout
     15 android.widget.TableRow
     15 android.widget.RadioGroup
     15 android.widget.Chronometer
     13 android.widget.ViewFlipper
      9 android.widget.Spinner
      8 android.widget.ViewSwitcher
      8 android.widget.TextSwitcher
      8 android.widget.SurfaceView
      8 android.widget.CheckedTextView
      8 android.preference.PreferenceFrameLayout
      7 android.widget.TwoLineListItem
      5 android.widget.TableLayout
      5 android.widget.EditText
      3 android.widget.TabWidget
      3 android.widget.TabHost
      2 android.widget.ZoomButton
      2 android.widget.TextureView
      2 android.widget.ExpandableListView
      2 android.webkit.TextureView
      2 android.view.TextureView
      2 android.app.TextureView
      1 android.widget.WebView
      1 android.widget.ViewAnimator
      1 android.widget.TextClock
      1 android.widget.AutoCompleteTextView
      1 android.webkit.WebView
      1 android.webkit.SurfaceView
      1 android.view.SurfaceView
      1 android.app.SurfaceView

Unfortunately, replacing reflection with MethodHandle constructors is
counter-productive in terms of performance:

    Constructor direct:             create=5    invoke=42
    Constructor reflection:         create=310  invoke=433
    Constructor MethodHandle:       create=3283 invoke=3489
    Constructor MethodHandle-exact: create=3273 invoke=3453

To reduce the performance impact of slow reflection, we can leverage the
fact that the most frequently-inflated classes are from the framework,
and hard-code direct constructor references for them in a switch-case
block. Reflection will automatically be used as a fallback for custom
app views.

Test: simpleperf record -a; verify that Constructor.newInstance ->
      LayoutInflater no longer appears at the top under
      art_jni_trampoline
Change-Id: I8fcc0e05813ff9ecf1eddca3cc6920e747adf4fc
  • Loading branch information
kdrag0n committed Oct 16, 2021
1 parent 1fa2663 commit 47c2c1b
Showing 1 changed file with 171 additions and 46 deletions.
217 changes: 171 additions & 46 deletions core/java/android/view/LayoutInflater.java
Original file line number Diff line number Diff line change
Expand Up @@ -804,67 +804,75 @@ public final View createView(@NonNull Context viewContext, @NonNull String name,
throws ClassNotFoundException, InflateException {
Objects.requireNonNull(viewContext);
Objects.requireNonNull(name);
Constructor<? extends View> constructor = sConstructorMap.get(name);
if (constructor != null && !verifyClassLoader(constructor)) {
constructor = null;
sConstructorMap.remove(name);
}
String prefixedName = prefix != null ? (prefix + name) : name;
Class<? extends View> clazz = null;

try {
Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);

if (constructor == null) {
// Class not found in the cache, see if it's real, and try to add it
clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
mContext.getClassLoader()).asSubclass(View.class);

if (mFilter != null && clazz != null) {
boolean allowed = mFilter.onLoadClass(clazz);
if (!allowed) {
failNotAllowed(name, prefix, viewContext, attrs);
}
// Opportunistically create view directly instead of using reflection
View view = tryCreateViewDirect(prefixedName, viewContext, attrs);
if (view == null) {
Constructor<? extends View> constructor = sConstructorMap.get(name);
if (constructor != null && !verifyClassLoader(constructor)) {
constructor = null;
sConstructorMap.remove(name);
}
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
sConstructorMap.put(name, constructor);
} else {
// If we have a filter, apply it to cached constructor
if (mFilter != null) {
// Have we seen this name before?
Boolean allowedState = mFilterMap.get(name);
if (allowedState == null) {
// New class -- remember whether it is allowed
clazz = Class.forName(prefix != null ? (prefix + name) : name, false,
mContext.getClassLoader()).asSubclass(View.class);

boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
mFilterMap.put(name, allowed);

if (constructor == null) {
// Class not found in the cache, see if it's real, and try to add it
clazz = Class.forName(prefixedName, false,
mContext.getClassLoader()).asSubclass(View.class);

if (mFilter != null && clazz != null) {
boolean allowed = mFilter.onLoadClass(clazz);
if (!allowed) {
failNotAllowed(name, prefix, viewContext, attrs);
}
} else if (allowedState.equals(Boolean.FALSE)) {
failNotAllowed(name, prefix, viewContext, attrs);
}
constructor = clazz.getConstructor(mConstructorSignature);
constructor.setAccessible(true);
sConstructorMap.put(name, constructor);
} else {
// If we have a filter, apply it to cached constructor
if (mFilter != null) {
// Have we seen this name before?
Boolean allowedState = mFilterMap.get(name);
if (allowedState == null) {
// New class -- remember whether it is allowed
clazz = Class.forName(prefixedName, false,
mContext.getClassLoader()).asSubclass(View.class);

boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
mFilterMap.put(name, allowed);
if (!allowed) {
failNotAllowed(name, prefix, viewContext, attrs);
}
} else if (allowedState.equals(Boolean.FALSE)) {
failNotAllowed(name, prefix, viewContext, attrs);
}
}
}
}

Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = viewContext;
Object[] args = mConstructorArgs;
args[1] = attrs;
Object lastContext = mConstructorArgs[0];
mConstructorArgs[0] = viewContext;
Object[] args = mConstructorArgs;
args[1] = attrs;

try {
final View view = constructor.newInstance(args);
if (view instanceof ViewStub) {
// Use the same context when inflating ViewStub later.
final ViewStub viewStub = (ViewStub) view;
viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
try {
view = constructor.newInstance(args);
} finally {
mConstructorArgs[0] = lastContext;
}
return view;
} finally {
mConstructorArgs[0] = lastContext;
}

if (view instanceof ViewStub) {
// Use the same context when inflating ViewStub later.
final ViewStub viewStub = (ViewStub) view;
viewStub.setLayoutInflater(cloneInContext((Context) viewContext));
}

return view;
} catch (NoSuchMethodException e) {
final InflateException ie = new InflateException(
getParserStateDescription(viewContext, attrs)
Expand Down Expand Up @@ -1363,4 +1371,121 @@ protected void dispatchDraw(Canvas canvas) {
}
}
}

// Some of the views included here are deprecated, but apps still use them.
@SuppressWarnings("deprecation")
private static View tryCreateViewDirect(String name, Context context, AttributeSet attributeSet) {
// This contains all the framework views used in a set of 113 real-world apps, sorted by
// number of occurrences. While views with only 1 occurrence are unlikely to be worth
// optimizing, it doesn't hurt to include them because switch-case is compiled into a table
// lookup after calling String#hashCode().
switch (name) {
case "android.widget.LinearLayout": // 13486 occurrences
return new android.widget.LinearLayout(context, attributeSet);
case "android.widget.View": // 6930 occurrences
case "android.webkit.View": // 63 occurrences
case "android.view.View": // 63 occurrences
case "android.app.View": // 62 occurrences
return new android.view.View(context, attributeSet);
case "android.widget.FrameLayout": // 6447 occurrences
return new android.widget.FrameLayout(context, attributeSet);
case "android.widget.ViewStub": // 5613 occurrences
case "android.view.ViewStub": // 228 occurrences
case "android.app.ViewStub": // 227 occurrences
case "android.webkit.ViewStub": // 226 occurrences
return new android.view.ViewStub(context, attributeSet);
case "android.widget.TextView": // 4722 occurrences
return new android.widget.TextView(context, attributeSet);
case "android.widget.ImageView": // 3044 occurrences
return new android.widget.ImageView(context, attributeSet);
case "android.widget.RelativeLayout": // 2665 occurrences
return new android.widget.RelativeLayout(context, attributeSet);
case "android.widget.Space": // 1694 occurrences
return new android.widget.Space(context, attributeSet);
case "android.widget.ProgressBar": // 770 occurrences
return new android.widget.ProgressBar(context, attributeSet);
case "android.widget.Button": // 382 occurrences
return new android.widget.Button(context, attributeSet);
case "android.widget.ImageButton": // 265 occurrences
return new android.widget.ImageButton(context, attributeSet);
case "android.widget.Switch": // 145 occurrences
return new android.widget.Switch(context, attributeSet);
case "android.widget.DateTimeView": // 117 occurrences
return new android.widget.DateTimeView(context, attributeSet);
case "android.widget.Toolbar": // 86 occurrences
return new android.widget.Toolbar(context, attributeSet);
case "android.widget.HorizontalScrollView": // 68 occurrences
return new android.widget.HorizontalScrollView(context, attributeSet);
case "android.widget.ScrollView": // 67 occurrences
return new android.widget.ScrollView(context, attributeSet);
case "android.widget.NotificationHeaderView": // 65 occurrences
case "android.webkit.NotificationHeaderView": // 65 occurrences
case "android.view.NotificationHeaderView": // 65 occurrences
case "android.app.NotificationHeaderView": // 65 occurrences
return new android.view.NotificationHeaderView(context, attributeSet);
case "android.widget.ListView": // 58 occurrences
return new android.widget.ListView(context, attributeSet);
case "android.widget.QuickContactBadge": // 50 occurrences
return new android.widget.QuickContactBadge(context, attributeSet);
case "android.widget.SeekBar": // 40 occurrences
return new android.widget.SeekBar(context, attributeSet);
case "android.widget.CheckBox": // 38 occurrences
return new android.widget.CheckBox(context, attributeSet);
case "android.widget.GridLayout": // 16 occurrences
return new android.widget.GridLayout(context, attributeSet);
case "android.widget.TableRow": // 15 occurrences
return new android.widget.TableRow(context, attributeSet);
case "android.widget.RadioGroup": // 15 occurrences
return new android.widget.RadioGroup(context, attributeSet);
case "android.widget.Chronometer": // 15 occurrences
return new android.widget.Chronometer(context, attributeSet);
case "android.widget.ViewFlipper": // 13 occurrences
return new android.widget.ViewFlipper(context, attributeSet);
case "android.widget.Spinner": // 9 occurrences
return new android.widget.Spinner(context, attributeSet);
case "android.widget.ViewSwitcher": // 8 occurrences
return new android.widget.ViewSwitcher(context, attributeSet);
case "android.widget.TextSwitcher": // 8 occurrences
return new android.widget.TextSwitcher(context, attributeSet);
case "android.widget.SurfaceView": // 8 occurrences
case "android.webkit.SurfaceView": // 1 occurrence
case "android.view.SurfaceView": // 1 occurrence
case "android.app.SurfaceView": // 1 occurrence
return new android.view.SurfaceView(context, attributeSet);
case "android.widget.CheckedTextView": // 8 occurrences
return new android.widget.CheckedTextView(context, attributeSet);
case "android.preference.PreferenceFrameLayout": // 8 occurrences
return new android.preference.PreferenceFrameLayout(context, attributeSet);
case "android.widget.TwoLineListItem": // 7 occurrences
return new android.widget.TwoLineListItem(context, attributeSet);
case "android.widget.TableLayout": // 5 occurrences
return new android.widget.TableLayout(context, attributeSet);
case "android.widget.EditText": // 5 occurrences
return new android.widget.EditText(context, attributeSet);
case "android.widget.TabWidget": // 3 occurrences
return new android.widget.TabWidget(context, attributeSet);
case "android.widget.TabHost": // 3 occurrences
return new android.widget.TabHost(context, attributeSet);
case "android.widget.ZoomButton": // 2 occurrences
return new android.widget.ZoomButton(context, attributeSet);
case "android.widget.TextureView": // 2 occurrences
case "android.webkit.TextureView": // 2 occurrences
case "android.app.TextureView": // 2 occurrences
case "android.view.TextureView": // 2 occurrences
return new android.view.TextureView(context, attributeSet);
case "android.widget.ExpandableListView": // 2 occurrences
return new android.widget.ExpandableListView(context, attributeSet);
case "android.widget.ViewAnimator": // 1 occurrence
return new android.widget.ViewAnimator(context, attributeSet);
case "android.widget.TextClock": // 1 occurrence
return new android.widget.TextClock(context, attributeSet);
case "android.widget.AutoCompleteTextView": // 1 occurrence
return new android.widget.AutoCompleteTextView(context, attributeSet);
case "android.widget.WebView": // 1 occurrence
case "android.webkit.WebView": // 1 occurrence
return new android.webkit.WebView(context, attributeSet);
}

return null;
}
}

0 comments on commit 47c2c1b

Please sign in to comment.