場(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ó)駿」
視頻列表有個(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):
不出所料有兩個(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;
}
可以看出是一個(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ì)象斤贰,變成了直接將findViewById
inLine到調(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
是在Fragment
的onViewCreated
的代碼中块饺,這個(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)
- 使用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獲取娇掏。
- 盡量避免在同一個(gè)頁(yè)面的不同級(jí)別的地方使用同樣的id垦缅,特別注意列表的item的id不要和外層的id重復(fù)。
-
import kotlinx.android.synthetic.xx
導(dǎo)入的只是符號(hào)引用驹碍,有可能聲明的是kotlinx.android.synthetic.a.view1
但是實(shí)際上代碼里面獲取的是kotlinx.android.synthetic.b.view1
壁涎。 - 使用Kotlin Synthetics獲取view可能會(huì)導(dǎo)致null pointer,特別是在某些回調(diào)函數(shù)里面view已經(jīng)釋放的情況下志秃。
- 在2020年11月怔球,Google官方宣布正式棄用Kotlin Android Extensions里面的Synthetics,而推薦使用Jecpack View Binding浮还,android-kotlin-extensions將會(huì)在2021年的9月左右移除竟坛,后續(xù)代碼盡量不要使用Kotlin Synthetics。