Kotlin升級(jí)1.5版本synthetic引發(fā)的血案分析

場(chǎng)景重現(xiàn)

因?yàn)轫?xiàng)目里面Kotlin版本還停留在1.4棵帽,看到1.5版本更新記錄提升了性能并且新加了一些特性面睛,準(zhǔn)備怒升級(jí)一波评腺。懷著開心的心情升級(jí)完之后,運(yùn)行起來(lái)就傻眼了官册!轉(zhuǎn)載請(qǐng)注明來(lái)源「申國(guó)駿」

1625820518128283

視頻列表有個(gè)浮層沒(méi)有隱藏逗载,就升級(jí)下Kotlin,居然還有這個(gè)問(wèn)題糠涛,真是太不可思議了!把Kotlin降級(jí)回去兼犯,然后就好了忍捡,確定是因?yàn)镵otlin升級(jí)導(dǎo)致的問(wèn)題。接下來(lái)就開始分析了切黔。

FindViewById砸脊?

第一反應(yīng)是找下代碼看看!

private fun tryHideTransitionImage() {
  // transition_image就是那個(gè)浮層
  if (transition_image.visibility != View.GONE) {
      transition_image.visibility = View.GONE
  }
}

debug一下绕娘,代碼運(yùn)行的順序在Kotlin升級(jí)前和升級(jí)后沒(méi)有區(qū)別脓规!然后想到的就是transition_image使用Kotlin synthetic獲取的栽连,是不是和findViewById有什么區(qū)別呢险领?于是改成了

private fun tryHideTransitionImage() {
  // transition_image就是那個(gè)浮層
  if (view.findViewById(R.id.transition_image).visibility != View.GONE) {
      view.findViewById(R.id.transition_image).visibility = View.GONE
  }
}

然而問(wèn)題還是一樣!果然問(wèn)題不是那么簡(jiǎn)單秒紧!我們使用[Stetho](Download (facebook.github.io))來(lái)看看這個(gè)頁(yè)面里面transition_image的狀態(tài):

WechatIMG1033

不出所料有兩個(gè)transition_image绢陌,一個(gè)是在外層FrameLayout底下的懸浮層,另一個(gè)是在Recyclerview的ItemView里面的視頻封面熔恢。懸浮層的屬性顯示確實(shí)是沒(méi)有隱藏脐湾。這個(gè)時(shí)候就要看看FindViewById的原理了,我們看下findViewById的源碼:

// View.java
public final <T extends View> T findViewById(@IdRes int id) {
  if (id == NO_ID) {
    return null;
  }
  return findViewTraversal(id);
}

在ViewGroup里面重寫了findViewTraversal方法

findViewTraversalprotected <T extends View> T findViewTraversal(@IdRes int id) {
  if (id == mID) {
    return (T) this;
  }

  final View[] where = mChildren;
  final int len = mChildrenCount;

  for (int i = 0; i < len; i++) {
    View v = where[i];

    if ((v.mPrivateFlags & PFLAG_IS_ROOT_NAMESPACE) == 0) {
      v = v.findViewById(id);

      if (v != null) {
        return (T) v;
      }
    }
  }

  return null;
}
image-20210714164915387

可以看出是一個(gè)深度優(yōu)先搜索算法叙淌,因此在我們使用[Stetho](Download (facebook.github.io))時(shí)看到的View樹里面秤掌,會(huì)先遍歷到RecyclerView底下的視頻封面愁铺,因此如果直接使用findViewById(R.id.transition_image)來(lái)隱藏浮層的話,拿到的并不是浮層闻鉴。

那么為什么1.4版本的Kotlin里面不會(huì)出問(wèn)題呢茵乱?這個(gè)時(shí)候得分析下編譯之后的ByteCode了。

ByteCode分析

private fun tryHideTransitionImage() {
  // transition_image就是那個(gè)浮層
  if (transition_image.visibility != View.GONE) {
      transition_image.visibility = View.GONE
  }
}

同樣的這段代碼孟岛,在使用Kotlin1.4版本的android extensions compile編譯之后的dex的二進(jìn)制代碼如下:

.method private final tryHideTransitionImage()V
          .registers 4
00000000  sget                v0, R$id->transition_image:I
00000004  invoke-virtual      CommunityVideoListFragment->_$_findCachedViewById(I)View, p0, v0
0000000A  move-result-object  v0
0000000C  check-cast          v0, ImageView
00000010  const-string        v1, "transition_image"
00000014  invoke-static       Intrinsics->checkNotNullExpressionValue(Object, String)V, v0, v1
0000001A  invoke-virtual      ImageView->getVisibility()I, v0
00000020  move-result         v0
00000022  const/16            v2, 8
00000026  if-eq               v0, v2, :46
:2A
0000002A  sget                v0, R$id->transition_image:I
0000002E  invoke-virtual      CommunityVideoListFragment->_$_findCachedViewById(I)View, p0, v0
00000034  move-result-object  v0
00000036  check-cast          v0, ImageView
0000003A  invoke-static       Intrinsics->checkNotNullExpressionValue(Object, String)V, v0, v1
00000040  invoke-virtual      ImageView->setVisibility(I)V, v0, v2
:46
00000046  return-void
.end method

.method public _$_findCachedViewById(I)View
          .registers 4
00000000  iget-object         v0, p0, CommunityVideoListFragment->_$_findViewCache:HashMap
00000004  if-nez              v0, :16
:8
00000008  new-instance        v0, HashMap
0000000C  invoke-direct       HashMap-><init>()V, v0
00000012  iput-object         v0, p0, CommunityVideoListFragment->_$_findViewCache:HashMap
:16
00000016  iget-object         v0, p0, CommunityVideoListFragment->_$_findViewCache:HashMap
0000001A  invoke-static       Integer->valueOf(I)Integer, p1
00000020  move-result-object  v1
00000022  invoke-virtual      HashMap->get(Object)Object, v0, v1
00000028  move-result-object  v0
0000002A  check-cast          v0, View
0000002E  if-nez              v0, :5C
:32
00000032  invoke-virtual      Fragment->getView()View, p0
00000038  move-result-object  v0
0000003A  if-nez              v0, :42
:3E
0000003E  const/4             p1, 0
00000040  return-object       p1
:42
00000042  invoke-virtual      View->findViewById(I)View, v0, p1
00000048  move-result-object  v0
0000004A  iget-object         v1, p0, CommunityVideoListFragment->_$_findViewCache:HashMap
0000004E  invoke-static       Integer->valueOf(I)Integer, p1
00000054  move-result-object  p1
00000056  invoke-virtual      HashMap->put(Object, Object)Object, v1, p1, v0
:5C
0000005C  return-object       v0
.end method

我們查看下dex指令文檔瓶竭,對(duì)這段dex二進(jìn)制翻譯一下:

private final void tryHideTransitionImage() {
  ImageView v0 = (ImageView)this._$_findCachedViewById(id.transition_image);
  Intrinsics.checkNotNullExpressionValue(v0, "transition_image");
  if(v0.getVisibility() != View.Gone) {
    ImageView v0_1 = (ImageView)this._$_findCachedViewById(id.transition_image);
    Intrinsics.checkNotNullExpressionValue(v0_1, "transition_image");
    v0_1.setVisibility(View.Gone);
  }
}

public View _$_findCachedViewById(int arg3) {
  if(this._$_findViewCache == null) {
    this._$_findViewCache = new HashMap();
  }
  View v0 = (View)this._$_findViewCache.get(Integer.valueOf(arg3));
  if(v0 == null) {
    View v0_1 = this.getView();
    if(v0_1 == null) {
      return null;
    }
    v0 = v0_1.findViewById(arg3);
    this._$_findViewCache.put(Integer.valueOf(arg3), v0);
  }
  return v0;
}

在升級(jí)到Kotlin1.5版本后,二進(jìn)制代碼如下:

.method private final tryHideTransitionImage()V
          .registers 4
00000000  invoke-virtual      CommunityVideoListFragment->getView()View, p0
00000006  move-result-object  v0
00000008  const/4             v1, 0
0000000A  if-nez              v0, :12
:E
0000000E  move-object         v0, v1
00000010  goto                :1E
:12
00000012  sget                v2, R$id->transition_image:I
00000016  invoke-virtual      View->findViewById(I)View, v0, v2
0000001C  move-result-object  v0
:1E
0000001E  check-cast          v0, ImageView
00000022  invoke-virtual      ImageView->getVisibility()I, v0
00000028  move-result         v0
0000002A  const/16            v2, 8
0000002E  if-eq               v0, v2, :56
:32
00000032  invoke-virtual      CommunityVideoListFragment->getView()View, p0
00000038  move-result-object  v0
0000003A  if-nez              v0, :40
:3E
0000003E  goto                :4C
:40
00000040  sget                v1, R$id->transition_image:I
00000044  invoke-virtual      View->findViewById(I)View, v0, v1
0000004A  move-result-object  v1
:4C
0000004C  check-cast          v1, ImageView
00000050  invoke-virtual      ImageView->setVisibility(I)V, v1, v2
:56
00000056  return-void
.end method

翻譯成Java代碼如下:

private final tryHideTransitionImage() {
    Object v0 = this.getView();
    if (v0 != null) {
        v0 = v0.findViewById(R.id.transition_image);
    } else {
        v0 = null;
    }
    if (((ImageView) v0).getVisibility != View.Gone) {
        v0 = this.getView();
        if (v0 != null) {
            Object v1 = v0.findViewById(R.id.transition_image);
            ((ImageView) v1).setVisibility(View.Gone)
        }
    }
}

可以看出在1.5版本之后渠羞,Kotln Synthetic由原來(lái)的生成一個(gè)_findCachedViewById來(lái)保存View對(duì)象斤贰,變成了直接將findViewByIdinLine到調(diào)用的地方,沒(méi)有使用view cache保存對(duì)象次询。這個(gè)改動(dòng)的

因此我們上面遇到的問(wèn)題也能得到比較清晰的答案了荧恍。因?yàn)樵?.4版本里面,代碼里面的transition_image指的是第一次調(diào)用的對(duì)象渗蟹,而我們發(fā)現(xiàn)代碼里面第一次調(diào)用transition_image是在FragmentonViewCreated的代碼中块饺,這個(gè)時(shí)候由于列表還沒(méi)加載,所以獲取到的就是外層的浮層雌芽,之后對(duì)transitoin_image的調(diào)用都是指向這個(gè)浮層對(duì)象授艰,因此沒(méi)有問(wèn)題。而在升級(jí)到1.5版本之后世落,由于view cache機(jī)制改成了直接findViewById淮腾,因此在列表加載之后再獲取transition_image獲取到的就是列表里面的封面對(duì)象,導(dǎo)致了浮層沒(méi)有正常隱藏屉佳。

Kotlin Synthetic原理

在知道問(wèn)題的答案之后谷朝,我們?cè)龠M(jìn)一步看看Kotlin Synthetic是怎么生成這些代碼的。

override fun generateClassSyntheticParts(codegen: ImplementationBodyCodegen) {
    val classBuilder = codegen.v
    val targetClass = codegen.myClass as? KtClass ?: return

        // 沒(méi)有enable的話不生成
    if (!isEnabled(targetClass)) return

    val container = codegen.descriptor
    if (container.kind != ClassKind.CLASS && container.kind != ClassKind.OBJECT) return

    val containerOptions = ContainerOptionsProxy.create(container)
    // 判斷目標(biāo)是否Framgent或者Activity等需要生成cache的類
    if (containerOptions.getCacheOrDefault(targetClass) == NO_CACHE) return

        // 如果是LayoutContainer則需要開啟experiment特性才會(huì)生成cache
    if (containerOptions.containerType == LAYOUT_CONTAINER && !isExperimental(targetClass)) {
        return
    }

    val context = SyntheticPartsGenerateContext(classBuilder, codegen.state, container, targetClass, containerOptions)
    // 生成_findCachedViewById方法
    context.generateCachedFindViewByIdFunction()
    context.generateClearCacheFunction()
    context.generateCacheField()

    if (containerOptions.containerType.isFragment) {
        val classMembers = container.unsubstitutedMemberScope.getContributedDescriptors()
        val onDestroy = classMembers.firstOrNull { it is FunctionDescriptor && it.isOnDestroyFunction() }
        if (onDestroy == null) {
            context.generateOnDestroyFunctionForFragment()
        }
    }
}
private fun SyntheticPartsGenerateContext.generateCachedFindViewByIdFunction() {
  val containerAsmType = state.typeMapper.mapClass(container)

    val viewType = Type.getObjectType("android/view/View")

    val methodVisitor = classBuilder.newMethod(
    JvmDeclarationOrigin.NO_ORIGIN, ACC_PUBLIC, CACHED_FIND_VIEW_BY_ID_METHOD_NAME, "(I)Landroid/view/View;", null, null)
    methodVisitor.visitCode()
    val iv = InstructionAdapter(methodVisitor)

    val cacheImpl = CacheMechanism.get(containerOptions.getCacheOrDefault(classOrObject), iv, containerAsmType)

    fun loadId() = iv.load(1, Type.INT_TYPE)

    // Get cache property
    cacheImpl.loadCache()

    val lCacheNonNull = Label()
    iv.ifnonnull(lCacheNonNull)

    // Init cache if null
    cacheImpl.initCache()

    // Get View from cache
    iv.visitLabel(lCacheNonNull)
    cacheImpl.loadCache()
    loadId()
    cacheImpl.getViewFromCache()
    iv.checkcast(viewType)
    iv.store(2, viewType)

    val lViewNonNull = Label()
    iv.load(2, viewType)
    iv.ifnonnull(lViewNonNull)

    // Resolve View via findViewById if not in cache
    iv.load(0, containerAsmType)

    val containerType = containerOptions.containerType
    // 根據(jù)不同的類型獲取root View
    when (containerType) {
    AndroidContainerType.ACTIVITY, AndroidContainerType.ANDROIDX_SUPPORT_FRAGMENT_ACTIVITY, AndroidContainerType.SUPPORT_FRAGMENT_ACTIVITY, AndroidContainerType.VIEW, AndroidContainerType.DIALOG -> {
      loadId()
        iv.invokevirtual(containerType.internalClassName, "findViewById", "(I)Landroid/view/View;", false)
    }
    AndroidContainerType.FRAGMENT, AndroidContainerType.ANDROIDX_SUPPORT_FRAGMENT, AndroidContainerType.SUPPORT_FRAGMENT, LAYOUT_CONTAINER -> {
      if (containerType == LAYOUT_CONTAINER) {
        iv.invokeinterface(containerType.internalClassName, "getContainerView", "()Landroid/view/View;")
      } else {
        iv.invokevirtual(containerType.internalClassName, "getView", "()Landroid/view/View;", false)
      }

      iv.dup()
        val lgetViewNotNull = Label()
        iv.ifnonnull(lgetViewNotNull)

        // Return if getView() is null
        iv.pop()
        iv.aconst(null)
        iv.areturn(viewType)

        // Else return getView().findViewById(id)
        iv.visitLabel(lgetViewNotNull)
        loadId()
        iv.invokevirtual("android/view/View", "findViewById", "(I)Landroid/view/View;", false)
    }
    else -> throw IllegalStateException("Can't generate code for $containerType")
  }
  iv.store(2, viewType)

    // Store resolved View in cache
    cacheImpl.loadCache()
    loadId()
    cacheImpl.putViewToCache { iv.load(2, viewType) }

  iv.visitLabel(lViewNonNull)
    iv.load(2, viewType)
    iv.areturn(viewType)

    FunctionCodegen.endVisit(methodVisitor, CACHED_FIND_VIEW_BY_ID_METHOD_NAME, classOrObject)
}

這部分代碼在1.4和1.5的版本之間并沒(méi)有任何區(qū)別武花,那究竟是什么導(dǎo)致Kotlin1.5版本不生成_findCachedViewById方法呢圆凰?

這個(gè)時(shí)候,我們?cè)谑褂肒otlin1.5版本的基礎(chǔ)上体箕,通過(guò)在build.gradle文件中专钉,加入下面這段代碼,會(huì)發(fā)現(xiàn)_findCachedViewById方法會(huì)繼續(xù)生成累铅。而下面這段代碼的意思是使用舊的JVM編譯器跃须。

tasks.withType(org.jetbrains.kotlin.gradle.dsl.KotlinJvmCompile) {
    kotlinOptions.useOldBackend = true
}

因此,可以推斷是Kotin1.5版本中使用了新的JVM IR編譯器導(dǎo)致的娃兽。
報(bào)了個(gè)Bug給Jetbrains菇民,后續(xù)保持關(guān)注:https://youtrack.jetbrains.com/issue/KT-47733

注意事項(xiàng)

  1. 使用synthetic需要注意在1.5之前是使用cache機(jī)制的,在一個(gè)類里面使用synthetic獲取view會(huì)按照第一個(gè)獲取到的view為準(zhǔn),因此如果一個(gè)類里面對(duì)應(yīng)的viewid有重復(fù)的話第练,會(huì)以第一個(gè)為準(zhǔn)阔馋。在1.5之后,就是每個(gè)地方都通過(guò)findviewbyid獲取娇掏。
  2. 盡量避免在同一個(gè)頁(yè)面的不同級(jí)別的地方使用同樣的id垦缅,特別注意列表的item的id不要和外層的id重復(fù)。
  3. import kotlinx.android.synthetic.xx導(dǎo)入的只是符號(hào)引用驹碍,有可能聲明的是kotlinx.android.synthetic.a.view1但是實(shí)際上代碼里面獲取的是kotlinx.android.synthetic.b.view1壁涎。
  4. 使用Kotlin Synthetics獲取view可能會(huì)導(dǎo)致null pointer,特別是在某些回調(diào)函數(shù)里面view已經(jīng)釋放的情況下志秃。
  5. 在2020年11月怔球,Google官方宣布正式棄用Kotlin Android Extensions里面的Synthetics,而推薦使用Jecpack View Binding浮还,android-kotlin-extensions將會(huì)在2021年的9月左右移除竟坛,后續(xù)代碼盡量不要使用Kotlin Synthetics。

參考

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末钧舌,一起剝皮案震驚了整個(gè)濱河市担汤,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌洼冻,老刑警劉巖崭歧,帶你破解...
    沈念sama閱讀 206,378評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異撞牢,居然都是意外死亡率碾,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,356評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門屋彪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)所宰,“玉大人,你說(shuō)我怎么就攤上這事畜挥∽兄啵” “怎么了?”我有些...
    開封第一講書人閱讀 152,702評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵蟹但,是天一觀的道長(zhǎng)躯泰。 經(jīng)常有香客問(wèn)我,道長(zhǎng)矮湘,這世上最難降的妖魔是什么斟冕? 我笑而不...
    開封第一講書人閱讀 55,259評(píng)論 1 279
  • 正文 為了忘掉前任口糕,我火速辦了婚禮缅阳,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己十办,他們只是感情好秀撇,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,263評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著向族,像睡著了一般呵燕。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上件相,一...
    開封第一講書人閱讀 49,036評(píng)論 1 285
  • 那天再扭,我揣著相機(jī)與錄音,去河邊找鬼夜矗。 笑死泛范,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的紊撕。 我是一名探鬼主播罢荡,決...
    沈念sama閱讀 38,349評(píng)論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼对扶!你這毒婦竟也來(lái)了区赵?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,979評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤浪南,失蹤者是張志新(化名)和其女友劉穎笼才,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體络凿,經(jīng)...
    沈念sama閱讀 43,469評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡患整,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,938評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了喷众。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片各谚。...
    茶點(diǎn)故事閱讀 38,059評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖到千,靈堂內(nèi)的尸體忽然破棺而出昌渤,到底是詐尸還是另有隱情,我是刑警寧澤憔四,帶...
    沈念sama閱讀 33,703評(píng)論 4 323
  • 正文 年R本政府宣布膀息,位于F島的核電站,受9級(jí)特大地震影響了赵,放射性物質(zhì)發(fā)生泄漏潜支。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,257評(píng)論 3 307
  • 文/蒙蒙 一柿汛、第九天 我趴在偏房一處隱蔽的房頂上張望冗酿。 院中可真熱鬧,春花似錦、人聲如沸裁替。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,262評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)弱判。三九已至襟沮,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間昌腰,已是汗流浹背开伏。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評(píng)論 1 262
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留遭商,地道東北人硅则。 一個(gè)月前我還...
    沈念sama閱讀 45,501評(píng)論 2 354
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像株婴,于是被迫代替她去往敵國(guó)和親怎虫。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,792評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容