Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Android Viewビルドパフォーマンス向上について

Android Viewビルドパフォーマンス向上について

Android Viewビルドパフォーマンス向上について

C7a3ed4cf52ebd3ffc8fa9108cdcd533?s=128

interkenny

August 21, 2020
Tweet

Transcript

  1. @interkenny Android Engineer @Yumemi 趣味は旅行、スポーツ、ゲーム YUMEMI.apk #1 2020/08/21

  2. レイアウトパフォーマンス  レイアウト階層の最適化  Hierarchy Viewer でレイアウト検査を行う  低コストのレイアウトを採用(ConstraintLayout) 

    レイアウト再利用  ネストされた冗長なレイアウトを削除(include/mergeの利用)  オンデマンドロード  ViewStub の利用
  3. では本題  別観点でのレイアウトビルド  Layout xmlから xmlファイルの解析は重くない?

  4. 深く見てみる AppCompatActivity.java public class AppCompatActivity { ... @Override public void

    setContentView(View view) { getDelegate().setContentView(view); } ... } public abstract class LayoutInflater { public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) { final Resources res = getContext().getResources(); ... final XmlResourceParser parser = res.getLayout(resource); try { return inflate(parser, root, attachToRoot); } finally { parser.close(); } } } class AppCompatDelegateImpl { @Override public void setContentView(int resId) { ... LayoutInflater.from(mContext).inflate(resId, contentParent); ... } } Layoutファイル解析用パーサー登場 getLayout()にてResourcesImpl.loadXmlResourceParser()でXML parserを取得
  5. ResourcesImpl.java public class ResourcesImpl { ... XmlResourceParser loadXmlResourceParser(@NonNull String file,

    @AnyRes int id, int assetCookie,@NonNull String type) throws NotFoundException { if (id != 0) { try { synchronized (mCachedXmlBlocks) { ... // AssetManager final XmlBlock block = mAssets.openXmlBlockAsset(assetCookie, file); if (block != null) { final int pos = (mLastCachedXmlBlockIndex + 1) % num; mLastCachedXmlBlockIndex = pos; final XmlBlock oldBlock = cachedXmlBlocks[pos]; if (oldBlock != null) { oldBlock.close(); } cachedXmlBlockCookies[pos] = assetCookie; cachedXmlBlockFiles[pos] = file; cachedXmlBlocks[pos] = block; return block.newParser(); } } } catch (Exception e) { ... } } ... } } public final class AssetManager implements AutoCloseable { ... @NonNull XmlBlock openXmlBlockAsset(int cookie, @NonNull String fileName) throws IOException { Preconditions.checkNotNull(fileName, ”fileName“); synchronized (this) { ensureOpenLocked(); final long xmlBlock = nativeOpenXmlAsset(mObject, cookie, fileName); if (xmlBlock == 0) { throw new FileNotFoundException(“Asset XML file: ” + fileName); } final XmlBlock block = new XmlBlock(this, xmlBlock); incRefsLocked(block.hashCode()); return block; } } } NativeメソッドでXMLファイルをメモ リーに取り込み ここでIO処理発生
  6. LayoutInflater.java public abstract class LayoutInflater { public View inflate(@LayoutRes int

    resource, @Nullable ViewGroup root, boolean attachToRoot) { final Resources res = getContext().getResources(); ... final XmlResourceParser parser = res.getLayout(resource); try { return inflate(parser, root, attachToRoot); } finally { parser.close(); } } } public abstract class LayoutInflater { public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) { synchronized (mConstructorArgs) { ... try { // Temp is the root view that was found in the xml final View temp = createViewFromTag(root, name, inflaterContext, attrs); ViewGroup.LayoutParams params = null; if (root != null) { // Create layout params that match root, if supplied params = root.generateLayoutParams(attrs); if (!attachToRoot) { // Set the layout params for temp if we are not // attaching. (If we are, we use addView, below) temp.setLayoutParams(params); } } ... // We are supposed to attach all the views we found (int temp) // to root. Do that now. if (root != null && attachToRoot) { root.addView(temp, params); } ... } catch (XmlPullParserException e) { ... } finally { ... } return result; } } public abstract class LayoutInflater { View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,boolean ignoreThemeAttr) { ... View view = tryCreateView(parent, name, context, attrs); ... } public final View tryCreateView() { ... view = mFactory2.onCreateView(parent, name, context, attrs) ... return view } } AppCompatDelegateImpl. onCreateView() にて実現する
  7. 最後に AppCompatDelegateImpl.java class AppCompatDelegateImpl{ @Override public View createView(View parent, final

    String name, @NonNull Context context, @NonNull AttributeSet attrs) { ... return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext, IS_PRE_LOLLIPOP, /* Only read android:theme pre-L (L+ handles this anyway) */ true, /* Read read app:theme as a fallback at all times for legacy reasons */ VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */ ); } } public class AppCompatViewInflater { final View createView(View parent, final String name, @NonNull Context context, @NonNull AttributeSet attrs, boolean inheritContext, boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) { final Context originalContext = context; ... View view = null; switch (name) { case "TextView": view = createTextView(context, attrs); verifyNotNull(view, name); break; } ... return view; } protected AppCompatTextView createTextView(Context context, AttributeSet attrs) { return new AppCompatTextView(context, attrs); } ... } ここでViewのインスタンスを作成する
  8. 結論  レイアウト作成時、XMLファイルをメモリーに取り込み、IO処理が発生  メモリーにあるレイアウトタグをトラバースし、タグ名でReflectionにより対 象viewのインスタンスを作成  Viewツリーに追加 上記の処理はIOやReflectionにより、 時間かかる

    レイアウト複雑になると、更に
  9. 確認  View作成時間を計る class MainActivity : AppCompatActivity() { private var

    sum: Double = 0.0 @OptIn(ExperimentalTime::class) override fun onCreate(savedInstanceState: Bundle?) { LayoutInflaterCompat.setFactory2(LayoutInflater.from(this), object : LayoutInflater.Factory2 { override fun onCreateView(parent: View?,name: String?,context: Context?,attrs: AttributeSet? ): View? { val (view, duration) = measureTimedValue { delegate.createView(parent, name, requireNotNull(context), requireNotNull(attrs)) } sum += duration.inMilliseconds Log.v("onCreateView", "view=${view?.let { it::class.simpleName }} duration=$duration sum=$sum") return view } // Factoryでの利用、nullでいい override fun onCreateView(name: String,context: Context,attrs: AttributeSet): View? { return null } } ) super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) } } LayoutInflaterCompat. setFactory2()により、Viewのビルドプロセス時間を計ることができる super.onCreate(savedInstanceState)の前に上記のインタフェースを設定 Viewビルドプロセス実施
  10. 分析  5階層冗長なレイアウト構成を確認  ビルド時間は平均17ms  実際のプロジェクトではより複雑な構成になると、更に時間がかかる リファクタリング?

  11. リファクタリング  階層化 → フラット化  ConstraintLayout  共通コンポーネントの抽出 あまり変わらないなぁ…

  12. いや、別の視点で 分析した通り、今回IOやReflectionに対して何とかしたい

  13. プログラミングで  プログラミングでViewを作成しましょう  Kotlin DSLを適用 コンテナレイアウト.apply { 子コンテナ.apply {

    // 各種属性を設定 // ・・・ }.also { addView(it) } }
  14. ConstraintLayout { layout_width = match_parent layout_height = match_parent } inline

    fun ViewGroup.TextView(init: TextView.() -> Unit) = TextView(context).apply(init).also { addView(it) } ConstraintLayout { layout_width = match_parent layout_height = match_parent TextView { layout_width = wrap_content layout_height = wrap_content } } inline var View.background_color: String get() { return "" } set(value) { setBackgroundColor(Color.parseColor(value)) } ConstraintLayout { layout_width = match_parent layout_height = match_parent background_color = "#ffff00" }
  15. var RecyclerView.onItemClick: (View, Int) -> Unit get() { return {

    _, _ -> } } set(value) { setOnItemClickListener(value) } fun RecyclerView.setOnItemClickListener(listener: (View, Int) -> Unit) { addOnItemTouchListener(object : RecyclerView.OnItemTouchListener { val gestureDetector = GestureDetector(context, object : GestureDetector.OnGestureListener { override fun onShowPress(e: MotionEvent?) { } override fun onSingleTapUp(e: MotionEvent?): Boolean { e?.let { findChildViewUnder(it.x, it.y)?.let { child -> listener(child, getChildAdapterPosition(child)) } } return false } ・・・ }) override fun onTouchEvent(rv: RecyclerView, e: MotionEvent) { } override fun onInterceptTouchEvent(rv: RecyclerView, e: MotionEvent): Boolean { gestureDetector.onTouchEvent(e) return false } ・・・ }) } RecyclerViewにItemクリックリスナーを設定する RecyclerView { layout_id = "rvTest" layout_width = match_parent layout_height = 300 onItemClick = onListItemClick } val onListItemClick = { v: View, i: Int -> ・・・ }
  16. 平均3ms!!! スピードが早い

  17. 最後 これからは Jetpack Compose  プログラミングで一から作成するのが面倒  Kotlin DSLなら、Anko? 

    もうDeprecated
  18. ご清聴ありがとうございました!