Compose编程思想 -- Compose中重组风险和优化

前言

在上一篇文章Compose编程思想 -- 深入理解声明式UI的状态订阅与自动更新 中,我详细介绍了Compose作为声明式UI框架,如何完成数据的自动刷新,常用的就是通过mutableStateOfmutableStateListOf的订阅能力实现对数据变化的监听,那么本节将会介绍从触发重组到重组操作的执行过程。

1 重组的风险和优化

来看一个非常简单的例子,通过Text显示一个文案,当点击文案时,刷新为最新的数据。

kotlin 复制代码
setContent {
    ComposeStudyTheme {
        // 重组作用域
        var name by remember {
            mutableStateOf("Alex")
        }
        Text(text = name, Modifier.clickable {
            name = "Alex ++"
        })
    }
}

那么在这个过程中,当点击Text时,将name设置为新值,此时Text使用了name,那么Text所在的作用域,也就是ComposeStudyTheme会触发重组,当屏幕下一帧刷新的时候,重组作用域内的所有代码都会执行。

接下来,我们使用Column替换ComposeStudyTheme,如果按照我们的之前的设想,当name发生变化时,那么在只有Column大括号内部的代码会重组,然而是这样吗?

kotlin 复制代码
setContent {
    Log.d("TAG", "onCreate: composition 1")
    Column {
        Log.d("TAG", "onCreate: composition 2")
        // 重组作用域
        var name by remember {
            mutableStateOf("Alex")
        }
        Text(text = name, Modifier.clickable {
            name = "Alex ++"
        })
    }
}

通过打印日志发现,当name发生变化之后,在Column外层的组件也发生了重组,其实我们只需要Text刷新即可,但是换了Column之后,导致重组作用域变成了setContent,其实处理一些不必要的刷新,带来系统资源的浪费。

js 复制代码
2024-03-19 15:35:45.339 20899-20899 TAG                     com.lay.composestudy                 D  onCreate: composition 1
2024-03-19 15:35:45.340 20899-20899 TAG                     com.lay.composestudy                 D  onCreate: composition 2

那么为什么会这样呢?我们看下Column的源码:

kotlin 复制代码
@Composable
inline fun Column(
    modifier: Modifier = Modifier,
    verticalArrangement: Arrangement.Vertical = Arrangement.Top,
    horizontalAlignment: Alignment.Horizontal = Alignment.Start,
    content: @Composable ColumnScope.() -> Unit
) {
    val measurePolicy = columnMeasurePolicy(verticalArrangement, horizontalAlignment)
    Layout(
        content = { ColumnScopeInstance.content() },
        measurePolicy = measurePolicy,
        modifier = modifier
    )
}

我们发现Column是内联函数,这就意味着编译器在执行的时候,其实Column这个大括号是不存在的,相当于直接加到了setContent代码中,因此在判断重组作用域的时候,需要注意一下父容器是否为内联函数,如果为内联函数,那么重组作用域将会依次往上提升,直到达到非内联函数为止。

1.1 重组的风险

前面我们介绍了如何判断重组作用域的范围,因为Compose自身的特性,可能会导致重组作用域扩大,引发一些不必要的刷新,从而影响系统的性能。

kotlin 复制代码
setContent {
    Log.d("TAG", "onCreate: composition 1")
    NoParamsCompose()
    Column {
        Log.d("TAG", "onCreate: composition 2")
        // 重组作用域
        var name by remember {
            mutableStateOf("Alex")
        }
        Text(text = name, Modifier.clickable {
            name = "Alex ++"
        })
    }
}

@Composable
fun NoParamsCompose() {
    Log.d("TAG", "NoParamsCompose: ----composition")
    Text(text = "我没有参数")
}

看下上面的例子,既然使用了Column组件导致重组作用域变大,那么NoParamsCompose在重组的过程中必然会被执行,但是运行之后发现,NoParamsCompose在重组的时候并没有执行。

我们之前担心的问题并没有出现,是我们代码写的有问题吗?其实并不是,而是Compose自身的性能优化带来的。Compose在重建的过程中,会比较Composable函数的参数与上次是否发生了变化,如果没有发生变化,那么在重组的过程中就会跳过代码的执行。

NoParamsCompose函数没有参数,因此并没有参数变化这一说,因此在重组的过程中便会跳过执行。

kotlin 复制代码
@Composable
fun NoParamsCompose(content: String) {
    Log.d("TAG", "NoParamsCompose: ----composition")
    Text(text = "我有参数 $content")
}

现在我们对NoParamsCompose改造一下,加了一个参数,那么如果在代码中入参写死,重组的过程中依然会跳过;如果将入参改为name,那么在重组的过程中就会刷新组件。

kotlin 复制代码
setContent {
    Log.d("TAG", "onCreate: composition 1")
    NoParamsCompose("Alex ooo")
    Column {
        Log.d("TAG", "onCreate: composition 2")
        // 重组作用域
        var name by remember {
            mutableStateOf("Alex")
        }
        Text(text = name, Modifier.clickable {
            name = "Alex ++"
        })
    }
}

那么问题来了!

1.2 如何判断Composable函数入参是否变化?

其实在Compose当中,是通过结构性判断Composable函数的入参是否发生变化,即通过equals来判断内容是否发生变化,如果没有变化,那么在重组的过程中就会跳过。

前面我们验证了基本数据类型,那么如果是实体对象,还能经得起验证吗?

kotlin 复制代码
data class Weather(
    val city:String,
    val temperature:Int
)

通过ShowWeather可组合函数来展示一个地区的温度,当点击Text触发重组之后,即便是每次调用ShowWeather时,都会新建一个Weather对象,但是并没有触发重组,这就说明Compose对于入参的判断就是通过结构化检测,即equals检测。

kotlin 复制代码
setContent {
    Log.d("TAG", "onCreate: composition 1")
    Column {
        Log.d("TAG", "onCreate: composition 2")
        ShowWeather(weather = Weather("北京", 12))
        // 重组作用域
        var name by remember {
            mutableStateOf("Alex")
        }
        Text(text = name, Modifier.clickable {
            name = "Alex ++"
        })
    }
}

@Composable
fun ShowWeather(weather: Weather) {
    Log.d("TAG", "ShowWeather: ----composition")
    Column {
        Text(text = "城市:${weather.city}")
        Text(text = "温度:${weather.temperature}")
    }
}

何为equals检测,就是检查Weather的成员变量前后是否发生了变化,虽然前后两次对象的引用都发生了变化,但是内容是一致的,data class是默认重写了equals方法,不需要我们自己手动处理

kotlin 复制代码
class Weather(
    val city: String,
    val temperature: Int
) {
    override fun equals(other: Any?): Boolean {
        return city == (other as Weather).city
                && temperature == (other as Weather).temperature
    }

    override fun hashCode(): Int {
        return super.hashCode()
    }
}

但是当我们将参数的val变为var之后,发现在重组的时候,ShowWeather可组合函数内部的代码被执行了,但是内容并没有发生变化。

kotlin 复制代码
data class Weather(
    var city:String, 
    var temperature:Int
)

其实这也是Compose做的一个安全机制 ,因为当参数变为var的时候,它可以在任何地方都被修改,它不能保证一直不变,因此Compose会无脑刷新页面,从而保证一直持有最新的值

kotlin 复制代码
var w = Weather("北京", 12)
val w2 = Weather("北京", 12)
val w3 = Weather("北京", 12)
w = w2

setContent {
    Log.d("TAG", "onCreate: composition 1")
    Column {
        Log.d("TAG", "onCreate: composition 2")
        ShowWeather(weather = w)
        // 重组作用域
        var name by remember {
            mutableStateOf("Alex")
        }
        Text(text = name, Modifier.clickable {
            name = "Alex ++"
            w = w3
        })
    }
}

w3.city = "上海"

1.3 @Stable注解使用

那我们会想,只要有被var修饰的成员,就认为这个类不可靠,在重组的时候Compose就会强制侵入本不需要刷新的Composable代码中,未免有些太暴力,那么有什么手段能够阻止吗?

kotlin 复制代码
@Stable
data class Weather(
    var city:String,
    var temperature:Int
)

Compose中将不可靠的对象,转换为可靠的对象,就是使用@Stable注解,它会告诉Compose编译器,Weather这个类从始至终都不会发生变化,当然这个保证需要我们自己做控制,保证不会在任何地方对其做修改。那么Compose就会在重组的时候,跳过执行。

但是作为程序员的我们,保证前后两个对象永远不发生变化,一直相等,其实还是很难的 ,而且在多方合作中也很容易打破这个规则,所以我们的前提是先保证安全性,然后再考虑性能,因此不再重写equals方法,只要两个对象不相同就刷新,我们只保证同一个对象就跳过刷新即可

kotlin 复制代码
@Stable
class Weather(
    var city:String,
    var temperature:Int
)

因此不再使用data class(默认重写了equals),而是使用普通的class类即可。

kotlin 复制代码
//@Stable
class Weather(
    city: String,
    temperature: Int
) {
    // 通过mutableStateOf修饰
    
    var city by mutableStateOf(city)
    var temperature by mutableStateOf(temperature)
}

如果我们不想使用@Stable注解,那么对于类中的public成员变量使用mutableStateOf存储,那么Compose也会认为这个类是可靠的,在重组的时候,会跳过执行。

kotlin 复制代码
setContent {
    Log.d("TAG", "onCreate: composition 1")
    Column {
        Log.d("TAG", "onCreate: composition 2")
        ShowWeather(weather = w)
        // 重组作用域
        var name by remember {
            mutableStateOf("Alex")
        }
        Text(text = name, Modifier.clickable {
            name = "Alex ++"
            w.city = "上海"
        })
    }
}

而且,在类中的成员变量发生变化的时候,也会触发重组进行刷新,所以使用这种写法的好处就是:

  • 在成员变量不变的情况下是稳定的,recomposition的过程中可以跳过;
  • 在成员变量变了的情况下,会触发重组,进行页面的刷新;

所以@Stable不建议使用得原因:因为我们自己无法保证做到完全的不可变。

2 derivedStateOf和remember的比较

在之前的文章中,我介绍了remember的使用场景,主要用于修饰mutableStateOf,防止被多次初始化影响界面刷新,这一小节中将会介绍derivedStateOf的用法,以及和remember的区别。

2.1 derivedStateOf

derived词义为衍生的,派生的。derivedStateOf从官方意思来看:state就是由一个或者多个state对象 派生或者衍生出来的,当任意state对象发生变化时,衍生state对象都会重新计算,并拿到一个最新状态的值

kotlin 复制代码
fun <T> derivedStateOf(
    calculation: () -> T,
): State<T> = DerivedSnapshotState(calculation, null)

从源码看,当state对象发生变化时,会重新执行calculation中的代码。

kotlin 复制代码
@Composable
fun ShowCity(data: MutableList<String> = mutableListOf("北京", "上海", "杭州")) {
    // 被移除的城市
    val removeCity = remember { mutableStateListOf<String>() }
    val showCity = remember {
        derivedStateOf {
            // 如果removeCity发生改变,这里会重新计算
            for (city in removeCity) {
                if (data.contains(city)) {
                    data.remove(city)
                }
            }
            //返回筛选后的data
            data
        }
    }
    LazyColumn {
        // 注意 LazyColumn不是内联函数,因此重组只会走这块区域
        items(showCity.value) {
            Text(text = it, Modifier.clickable {
                removeCity.add(it)
            })
        }
    }

}

所以根据derivedStateOf的特性,需要一个状态依赖另一个状态,我这里写了一个列表用于展示城市信息,当点击任意一个城市的时候,会将城市信息添加到removeCity中,从而触发了重组。

当重组的时候,执行到derivedStateOf,因为showCity的状态依赖于removeCity,而且在触发重组的时候,removeCity发生了变化,因此会执行derivedStateOf代码块中的代码,并将删除的城市信息从data中移除,此时showCity中的数据被删除了一条,再刷新展示的时候这一条数据将不再显示在屏幕上。

那么问题来了,为什么要使用derivedStateOf,好像使用mutableStateListOf也能实现这个功能,那么到底什么时候才会使用derivedStateOf

2.2 derivedStateOf的使用场景

首先我们先看一个简单的例子:

kotlin 复制代码
setContent {
    Log.d("TAG", "onCreate: composition 1")
    val data = remember {
        mutableStateListOf("北京", "上海", "杭州")
    }

    val showData = remember(data) {
        Log.d("TAG", "onCreate: add val")
        data.map {
            it.plus(" ++")
        }
    }

    Column {
        showData.forEach {
            Text(text = it,Modifier.clickable {
                data.add("苏州")
            })
        }
    }
}

关键看下showData这个变量,与以往不同的是,remember采用了下面的方法进行调用,将data属性作为了key。

kotlin 复制代码
/**
 * Remember the value returned by [calculation] if [key1] is equal to the previous composition,
 * otherwise produce and remember a new value by calling [calculation].
 */
@Composable
inline fun <T> remember(
    key1: Any?,
    crossinline calculation: @DisallowComposableCalls () -> T
): T {
    return currentComposer.cache(currentComposer.changed(key1), calculation)
}

官方的解释:

如果key值与组建的时候一样,没有发生变化,那么就返回缓存中的值,不会执行calculation中的代码;否则将会执行calculation中的代码,重新生成一个新的值。

看官方的解释,哎,好像跟derivedStateOf有点相似,derivedStateOf也是当内部的state发生变化的时候,执行calculation中的代码,那么我直接使用remember这种方式就好了,但是是这样的吗?

回到本小节开头的例子,当点击Column中的item时,会将data中添加一个元素,而showData则是将数据进行了一次转化,添加了++,当运行时我们发现并没有新增一条item。

data数据发生了变化,但是showData没有生成新的数据。那么这就是remember对于key是否相等的判断出现了问题,一般对象变化主要分为两种:

  • 直接赋值,例如给String赋值新值;
  • 对象内部变化,例如List增加或者删除一个元素。

就这个例子来看,data内部增加一个元素,但是data得引用还是没变,还是认为是一个对象,那么自然是无法生成一个新值,因此需要通过derivedStateOf装饰一下,用来处理因为内部数据发生变化时,remember认为没有发生变化,例如List的添加和删除操作。

kotlin 复制代码
setContent {
    Log.d("TAG", "onCreate: composition 1")
    val data = remember {
        mutableStateListOf("北京", "上海", "杭州")
    }

    val showData = remember(data) {
        Log.d("TAG", "onCreate: add val")
        derivedStateOf {
            data.map {
                it.plus(" ++")
            }
        }
    }

    Column {
        showData.value.forEach {
            Text(text = it, Modifier.clickable {
                data.add("苏州")
            })
        }
    }
}

2.3 derivedStateOf注意事项

先看看起来,derivedStateOf好像比remember(key)更加灵活,但是实际的场景中,我们能无脑使用derivedStateOf吗,看下面的例子:

kotlin 复制代码
@Composable
fun ShowUp() {
    val name = remember {
        mutableStateListOf("Alex", "Tom")
    }
    val showName = remember {
        derivedStateOf {
            name.map {
                it.uppercase()
            }
        }
    }
    Column {
        showName.value.forEach {
            Text(text = it, Modifier.clickable {
                name.add("Jerry")
            })
        }
    }
}

showName用于将英文变为大写,然后点击列表中的Item新增一个元素Jerry,此时name发生变化,Compose监听到从而导致showName重新计算,发生重组后新增了一条元素。

此时ShowUp是有状态的,那么我们将状态提升,将ShowUp变成无状态。

kotlin 复制代码
@Composable
fun ShowUp(name: String, onClick: (() -> Unit)? = null) {
    Log.d("TAG", "ShowUp: name-- $name")
    val showName by remember {
        // 第一次初始化会进来
        derivedStateOf { name.uppercase() }
    }
    Text(text = showName, Modifier.clickable {
        onClick?.invoke()
    })

}

首先我们先看一个最简单的例子,当点击某个Text之后,将Text的文案替换并且目的是将文案转成大写。

kotlin 复制代码
setContent {
    Log.d("TAG", "onCreate: composition 1")
    
    var name by remember {
        mutableStateOf("Alex")
    }
    ShowUp(name) {
        name = "Tom"
    }
}

我们在使用的时候如上所示,当点击Text时,我们将name赋值为Tom,此时会触发重组,重组作用域是调用name的父作用域,也就是setContent,此时ShowUp会再次执行,我们发现文案并没有像我们预想的那样变化。

为什么没有变化呢?其实当重组时执行ShowUp,此时作为参数传入的name只是一个代理值,就是一个String类型的value,那么在ShowUp中,derivedStateOf就无法对其进行监听,从而导致数据一直没有刷新。

解决这个问题的方案有两种:

  • ShowUp参数改为State对象,从而穿透Composable函数,在内部derivedStateOf就可以对其完成订阅,但不建议这么做,因为传值只能传递State对象,不具备通用性;
  • 采用remember(key)的方式订阅。
kotlin 复制代码
@Composable
fun ShowUp(name: String, onClick: (() -> Unit)? = null) {
    Log.d("TAG", "ShowUp: name-- $name")
    val showName = remember(name) {
        name.uppercase()
    }
    Text(text = showName, Modifier.clickable {
        onClick?.invoke()
    })

}

采用remember(key)的方式,当重组时进入ShowUp,remember会检查前后两次name是否发生变化,显然是变化了,因此页面完成了刷新。

回到本小节开始的例子:

kotlin 复制代码
@Composable
fun ShowUp(name: MutableList<String>, onClick: (() -> Unit)? = null) {
    val showName = remember(name) {
        name.map {
            it.uppercase()
        }
    }
    Column {
        showName.forEach {
            Text(text = it, Modifier.clickable {
                onClick?.invoke()
            })
        }
    }
}

当完成状态提升之后,在Composable函数内部还是采用remember(key)这种形式,很显然按照我们之前的结论,因为List修改属于内部改变,通过remember(key)校验前后两个key是一致的,从而不会刷新新值,需要使用derivedStateOf

kotlin 复制代码
// 注意这里不再是代理,而是=,会穿透Composable函数,derivedStateOf能够订阅这个变量
val name = remember {
    mutableStateListOf("Alex", "Tom")
}

ShowUp(name) {
    name.add("Jerry")
}

那么有些伙伴可能会有疑问,ShowUp函数参数为List类型,能够被derivedStateOf订阅?其实我们需要看传参的地方,不再是通过by拿到代理的value,而是货真价实的State对象,mutableStateListOf具备订阅的能力,而且继承自MutableList

kotlin 复制代码
@Composable
fun ShowUp(name: MutableList<String>, onClick: (() -> Unit)? = null) {
    val showName by remember {
        derivedStateOf {
            name.map {
                it.uppercase()
            }
        }
    }
    Column {
        showName.forEach {
            Text(text = it, Modifier.clickable {
                onClick?.invoke()
            })
        }
    }
}

那么这样真能做到万无一失吗👆🏻?

kotlin 复制代码
setContent {
    Log.d("TAG", "onCreate: composition 1")
    var count by remember {
        mutableStateOf(0)
    }
    val name = remember(count) {
        if (count > 5){
            mutableStateListOf("Alex", "Tom")
        }else{
            mutableStateListOf("北京","上海","广州")
        }
    }
    ShowUp(name) {
        count++
    }
}

假设对于ShowUp函数的入参有变化,那么derivedStateOf只会对第一个name订阅,后续name发生了变化都不会刷新,只会刷新第一个列表数据,因此官方的案例中建议我们这么写:

kotlin 复制代码
@Composable
fun ShowUp(name: MutableList<String>, onClick: (() -> Unit)? = null) {
    val showName by remember(name) {
        derivedStateOf {
            name.map {
                it.uppercase()
            }
        }
    }
    Column {
        showName.forEach {
            Text(text = it, Modifier.clickable {
                onClick?.invoke()
            })
        }
    }
}

derivedStateOfremember(key)一起使用,能够保证在name发生变化时,重新计算帮助derivedStateOf重新订阅,从而保证数据的准确刷新:不管是换了新对象,还是内部的状态发生了变化,Compose都可以监听到。

3 CompositionLocal

这又是一个新的概念,从字面意思上来看,就是Composition的局部变量

kotlin 复制代码
setContent {
    val name = "Alex"
    ShowText(name = name)
}
kotlin 复制代码
@Composable
fun ShowText(name: String) {
    Text(text = name)
}

setContent重组作用域内,name属于这个作用域内的局部变量,在这个作用域外的成员无法直接调用,包括ShowText可组合函数。

那么假设,我想把ShowText中的参数去掉,并且可以在ShowText中使用setContent重组作用域中的局部变量,看似天方夜谭,实则Compose已经帮我们做好了。

3.1 CompositionLocal的使用

CompositionLocalProvider可以看做是Composition局部变量的提供者,在其提供的content作用域内执行的Composable函数,局部变量具有穿透性,即在Composable函数中可以直接使用局部变量。

kotlin 复制代码
@Composable
@OptIn(InternalComposeApi::class)
fun CompositionLocalProvider(vararg values: ProvidedValue<*>, content: @Composable () -> Unit) {
    currentComposer.startProviders(values)
    content()
    currentComposer.endProviders()
}
  • 首先需要定义一个Composition局部变量,可以通过compositionLocalOf来创建。
kotlin 复制代码
val LocalName = compositionLocalOf<String> { error("LocalName is not init") }
  • 通过CompositionLocalProvider来给局部变量LocalName赋值,可以使用provides函数来为其赋值。这里需要注意,如果想要使用LocalName,那么就需要将@Composable函数放在CompositionLocalProvider作用域内。
kotlin 复制代码
CompositionLocalProvider(LocalName provides "Alex") {
    ShowText()
}
  • 获取值,可以通过CompositionLocal的current成员对象来获取。
kotlin 复制代码
@Composable
fun ShowText() {
    Text(text = LocalName.current)
}

以上就是CompositionLocal的简单使用,通过这种方式可以移除@Composable函数中的参数,直接调用局部变量的成员对象。

3.2 CompositionLocal有什么用?

CompositionLocal的使用中我们大概能够知道,CompositionLocal是一个具有穿透功能的局部变量,从而取代@Composable函数中的参数,在日常的开发中,我们已经熟悉了通过传值的方式进行透传,而如果使用CompositionLocal取代全部的入参,那么可能会影响到更大的范围,因为我们如果采用传参的方式,那么只会影响一个组合函数的内部执行,而CompositionLocal属于作用域内部的全局变量,在任意组合中改动,都可能会影响到其他的组合。

因此使用CompositionLocal一般是用来表示上下文、环境、主题等可能会在组合函数中使用到,也有可能使用不到,而一定会在组合函数中使用到的参数,请在组合函数中显示地声明。

kotlin 复制代码
val LocalActivity = compositionLocalOf<Activity> { error("LocalActivity error") }


CompositionLocalProvider(LocalActivity provides this) {
    ShowText2()
}

像如果使用LocalActivity来表示全局的上下文,那么在CompositionLocalProvider作用域内,所有的组合函数都可以拿到上下文使用,这也避免了我们在ShowText2中传入context参数,其实我们在开发中也是尽可能地减少这种参数传递,经常会在一个静态类中注入Context,然后直接调用,不知道是否有这么干的伙伴。

除了上下文,我们还可以定义主题,例如:

kotlin 复制代码
// 当前主题的局部变量
val LocalBackground = compositionLocalOf<Color> { error("LocalBackground error") }
kotlin 复制代码
CompositionLocalProvider(LocalBackground provides Color.Blue) {
    ShowBackground()
}


@Composable
fun ShowBackground() {
    Text(text = "检测文案展示", modifier = Modifier.background(LocalBackground.current))
}

假设这个页面的主题颜色为蓝色,那么就可以使用LocalBackground定义主题颜色,并且在ShowBackground中可以直接使用这个局部变量,好处是:当UI发生变更,主题色变为红色,那么只需要修改LocalBackground即可。

假设我们定义一个App的主题:

kotlin 复制代码
//定义局部变量
val LocalBackground = compositionLocalOf<Color> { error("") }
val LocalTextSize = compositionLocalOf<TextUnit> { error("") }
val LocalTextColor = compositionLocalOf<Color> { error("") }

@Composable
fun MyAppTheme(
    content: @Composable () -> Unit
) {
    CompositionLocalProvider(
        LocalBackground provides Color.Blue,
        LocalTextSize provides 20.sp,
        LocalTextColor provides Color.Black,
    ) {
        content.invoke()
    }
}

那么我们在使用的时候,就可以将其放在setContent之下,那么所有的组件都可以使用这些主题资源信息。

kotlin 复制代码
setContent {
    MyAppTheme {
        Column(Modifier.background(LocalBackground.current)) {
            Text(
                text = "测试主题1",
                color = LocalTextColor.current,
                fontSize = LocalTextSize.current
            )
            Text(
                text = "测试主题2",
                color = LocalTextColor.current,
                fontSize = LocalTextSize.current
            )
        }

    }
}
相关推荐
fatiaozhang95272 小时前
中兴云电脑W102D_晶晨S905X2_2+16G_mt7661无线_安卓9.0_线刷固件包
android·adb·电视盒子·魔百盒刷机·魔百盒固件
CYRUS_STUDIO3 小时前
Android APP 热修复原理
android·app·hotfix
鸿蒙布道师3 小时前
鸿蒙NEXT开发通知工具类(ArkTs)
android·ios·华为·harmonyos·arkts·鸿蒙系统·huawei
鸿蒙布道师3 小时前
鸿蒙NEXT开发网络相关工具类(ArkTs)
android·ios·华为·harmonyos·arkts·鸿蒙系统·huawei
大耳猫4 小时前
【解决】Android Gradle Sync 报错 Could not read workspace metadata
android·gradle·android studio
ta叫我小白4 小时前
实现 Android 图片信息获取和 EXIF 坐标解析
android·exif·经纬度
dpxiaolong5 小时前
RK3588平台用v4l工具调试USB摄像头实践(亮度,饱和度,对比度,色相等)
android·windows
tangweiguo030519876 小时前
Android 混合开发实战:统一 View 与 Compose 的浅色/深色主题方案
android
老狼孩111226 小时前
2025新版懒人精灵零基础及各板块核心系统视频教程-全分辨率免ROOT自动化开发
android·机器人·自动化·lua·脚本开发·懒人精灵·免root开发
打死不学Java代码7 小时前
PaginationInnerInterceptor使用(Mybatis-plus分页)
android·java·mybatis