小心kotlin中lambda引用外闭包可变变量的潜在问题
at 2023-04-01 22:56:37, by 鹏城奋青
在Compose,子级可组合项的事件lambda函数,使用了父级的var变量,子级事件函数中不断检测该变量的赋值情况。在debug版中运行正常,但release版即使该变量被修改,而子级获取的值永不变,这有可能是由于release优化引起的问题。谨慎起见,使用委托语法创建变量,该问题得以解决:
var snapshot by remember {
mutableStateOf<Bitmap?>(null)
}
以下为原代码:
package com.wisforce.app.ui.map
import android.annotation.SuppressLint
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Canvas
import android.graphics.Rect
import android.webkit.*
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import com.wisforce.app.R
import com.wisforce.app.chatty.Chatty
import com.wisforce.app.chatty.protol.LocationData
import com.wisforce.app.theme.CoreTheme
import com.wisforce.app.theme.widgets.CoreText
import com.wisforce.app.theme.widgets.CoreTextButton
import com.wisforce.app.widgets.dialog.rememberDialogState
import com.wisforce.app.widgets.misc.SyncIndicator
import com.wisforce.app.widgets.page.BackTitlePage
import kotlinx.coroutines.*
import org.bouncycastle.util.encoders.Base64
import java.io.File
@SuppressLint("SetJavaScriptEnabled")
@Composable
fun LocationChoosePage(locationData: LocationData, onFinish: (LocationChooseResult?)->Unit) {
var webView by remember{ mutableStateOf<WebView?> (null) }
var size by remember { mutableStateOf(IntSize.Zero) }
var address by remember { mutableStateOf("") }
var longitude by remember { mutableStateOf(locationData.longitude) }
var latitude by remember { mutableStateOf(locationData.latitude) }
val dialog = rememberDialogState()
val scope = rememberCoroutineScope()
val density = LocalDensity.current
// 注意此项:委托变量才能使下文等待循环正确,debug版没有问题,而release版是有问题的
var snapshotBitmap by remember { mutableStateOf<Bitmap?>(null) }
val context = LocalContext.current
LaunchedEffect(longitude, latitude) {
address = Chatty.getLocationAddress(
longitude = longitude,
latitude = latitude
) ?: ""
}
DisposableEffect(true) {
onDispose {
snapshotBitmap?.recycle()
}
}
fun prepareResult(): LocationChooseResult {
var cropFile: File? = null
snapshotBitmap?.let { snapshot ->
// 最好是能将webview截图下来,再获取中心部分即可,但实践发现无法截取
// webview中地图图像,有可能和地图使用webgl渲染有关,因此通过在html中使用html2canvas
// 渲染出地图部分,但仍然有一个问题,即html2canvas无法渲染一些元素,比如带position:fixed
// 等,无奈之下只用它截取地图底图,然后再将标记点绘制上去
// 截取中心300x140大小的部分
val cropWidth = with(density) { 300.dp.toPx().toInt() }
val cropHeight = with(density) { 140.dp.toPx().toInt() }
val cropBitmap = Bitmap.createBitmap(minOf(size.width, cropWidth),
minOf(size.height, cropHeight), Bitmap.Config.ARGB_8888)
val cropCanvas = Canvas(cropBitmap)
// 大图中获取的rect各项
val left = (snapshot.width - cropBitmap.width) / 2
val top = (snapshot.height - cropBitmap.height) / 2
val right = left + cropBitmap.width
val bottom = top + cropBitmap.height
cropCanvas.drawBitmap(
snapshot,
Rect(left, top, right, bottom),
Rect(0, 0, cropBitmap.width, cropBitmap.height),
null,
)
// 将标记绘制上去,因为html截图得不到标记元素
val markerBitmap = context.assets.open("poi_marker.png").use { si ->
BitmapFactory.decodeStream(si)
}
cropCanvas.drawBitmap(markerBitmap,
((cropWidth-markerBitmap.width)/2).toFloat(),
(cropHeight/2-markerBitmap.height).toFloat(),
null)
markerBitmap.recycle()
// 将图压缩回传
val tempFile = File.createTempFile("location", ".png")
tempFile.outputStream().use { so ->
if (cropBitmap.compress(Bitmap.CompressFormat.PNG, 10, so)) {
cropFile = tempFile
}
}
}
return LocationChooseResult(
picture = cropFile?.path ?: "",
location = LocationData().apply {
this.longitude = longitude
this.latitude = latitude
this.address = address
}
)
}
CoreTheme {
BackTitlePage(action = {
CoreTextButton(onClick = {
scope.launch(Dispatchers.IO) {
dialog.show { SyncIndicator() }
withContext(Dispatchers.Main) {
webView?.evaluateJavascript(
"takeSnapshot()",
null
)
}
// 5秒超时后,如果js截图还不完成就不管了。
var timeout = 5000
do {
if (snapshotBitmap != null) {
break
}
timeout -= 100
if(timeout <= 0) {
break
}
delay(100)
} while (isActive)
val result = prepareResult()
dialog.dismiss()
onFinish(result)
}
}, enabled = webView != null) {
CoreText(text = stringResource(id = R.string.send), )
}
}) {
var progress by remember { mutableStateOf(0) }
var done by remember { mutableStateOf(false) }
Box(modifier = Modifier
.fillMaxWidth()
.weight(1f)) {
AndroidView(
factory = {
WebView(it).apply {
webView = this
settings.javaScriptEnabled = true
settings.domStorageEnabled = true
settings.databaseEnabled = true
settings.blockNetworkImage = false
settings.blockNetworkLoads = false
settings.loadsImagesAutomatically = true
settings.allowUniversalAccessFromFileURLs = true
settings.allowFileAccessFromFileURLs = true
settings.allowFileAccess = true
settings.mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
webChromeClient = object : WebChromeClient() {
override fun onProgressChanged(view: WebView?, newProgress: Int) {
progress = newProgress
}
}
webViewClient = object: WebViewClient() {
override fun onPageFinished(view: WebView?, url: String?) {
super.onPageFinished(view, url)
done = true
}
}
class Bridge {
@JavascriptInterface
fun locationUpdate(lng: Double, lat: Double) {
longitude = lng
latitude = lat
}
@JavascriptInterface
fun snapshotUpdate(data: String) {
val index = data.indexOfFirst { c -> c == ',' }
val bytes = Base64.decode(data.substring(index+1, data.length))
snapshotBitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.size)
}
}
addJavascriptInterface(Bridge(), "bridge")
loadUrl(
Chatty.getLocationChooseUrl(
longitude = locationData.longitude,
latitude = locationData.latitude
)
)
}
},
modifier = Modifier
.fillMaxSize()
.onSizeChanged { size = it },
) {
it.webChromeClient = object : WebChromeClient() {
override fun onProgressChanged(view: WebView?, newProgress: Int) {
progress = newProgress
}
}
}
LaunchedEffect(done, size) {
if (done) {
val w = with(density) { size.width.toDp().value.toInt() }
val h = with(density) { size.height.toDp().value.toInt() }
withContext(Dispatchers.Main) {
webView?.evaluateJavascript(
"onSizeChanged(${w},${h})",
null
)
}
}
}
}
CoreText(text = address)
}
}
}