谷歌 I/O 深度解析:Android Jetpack 最新变化

Posted fundroid_方卓

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了谷歌 I/O 深度解析:Android Jetpack 最新变化相关的知识,希望对你有一定的参考价值。

5 月的山景城,一年一度的谷歌 I/O 开发者大会如期而至,由于当地疫情管制的放开,今年大会重回线下举行,真心希望国内的疫情也尽早结束。

今年的 I/O 大会既是谷歌各种新产品发布会,同时也是谷歌开发者们的技术交流会。不少 android 开发者希望通过本次 I/O 了解到有关 Jetpack 的最新动态。本文对这些内容进行了收集整理,一并分享给大家。

Jetpack Overview

Android Jetpack 为我们日常开发提供了便利的工具集以及最佳实践,根据本次大会上发布的数据,目前 GooglePlay Top1000 的应用中,使用至少 2 个以上 Jetpack 库的占比从 79% 提升到 90%

接下来从 Architecture,UI,Performance 和 Compose 等四个方向为大家介绍和点评 Jetpack 的最新变化。

1. Architecture

1.1 Room 2.4/2.5

Room 最新版本进入到 2.5。 2.5 没有新功能的引入,最大变化就是使用 Kotlin 进行了重写,借助 Kotlin 空安全等特性,代码将更加稳定可靠。未来还会有更多 Jetpack 库逐渐迁移至 Kotlin。

在功能方面,Room 自 2.4 以来引入了不少新特性:

KSP:新的注解处理器

Room 将注解处理方式从 KAPT 升级为 KSP(Kotlin Symbol Processing)。 KSP 作为新一代 Kotlin 注解处理器,1.0 版目前已正式发布,功能更加稳定,可以帮助你极大缩短项目的构建时间。KSP 的启用非常简单,只要像 KAPT 一样地配置即可:

plugins 
    //enable kapt
    id 'kotlin-kapt'
    //enable ksp
    id("com.google.devtools.ksp") 


dependencies 
    //...
    // use kapt
    kapt "androidx.room:room-compiler:$room_version"
    // use ksp
    ksp "androidx.room:room-compiler:$room_version"
    //...

Multi-map Relations:返回一对多数据

以前,Room 想要返回一对多的实体关系,需要额外增加类型定义,并通过 @Relatioin 进行关联,现在可以直接使用 Multi-map 返回,代码更加精简:

//before
data class ArtistAndSongs(
`   @Embedded
    val artist: Artist,
    @Relation(...)
    val songs: List<Song>
)

@Query("SELECT * FROM Artist")
fun getArtistAndSongs(): List<ArtistAndSongs>

//now
@Query("SELECT * FROM Artist JOIN Song ON Artist.artistName = Song.songArtistName")
fun getAllArtistAndTheirSongsList(): Map<Artist, List<Song>>

AutoMigrations:自动迁移

以前,当数据库表结构变化时,比如字段名之类的变化,需要手写 SQL 完成升级,而最近新增的 AutoMigrations 功能可以检测出两个表结构的区别,完成数据库字段的自动升级。

 @Database(
      version = MusicDatabase.LATEST_VERSION,
      entities =  Song.class,  Artist.class ,
      autoMigrations = 
          @AutoMigration (
              from = 1,
              to = 2
          )
      ,
      exportSchema = true
 )
 public abstract class MusicDatabase extends RoomDatabase 
   ...
 

1.2 Paging3

Paging3 相对于 Paging2 在使用方式上发生了较大变化。首先它提升了 Kotlin 协程的地位, 将 Flow 作为首选的分页数据的监听方案,其次它提升了 API 的医用型,降低了理解成本,同时它有着更丰富的能力,例如支持设置 Header 和 Footer等,建议大家尽可能地将项目中的 Paging2 升级到 Paging3。

简单易用的数据源

Paging2 的数据源有多种实现,PageKeyedDataSource, PositionalDataSource, ItemKeyedDataSource 等,需要我们根据场景做出不同选择 ,而 Paging3 在使用场景上进行了整合和简化,只提供一种数据源类型 PagingSource:

class MyPageDataSource(private val repo: DataRepository) : PagingSource<Int, Post>() 
    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Data> 
    try 
        val currentLoadingPageKey = params.key ?: 1  
        // 从 Repository 拉去数据
        val response = repo.getListData(currentLoadingPageKey)
      
        val prevKey = if (currentLoadingPageKey == 1) null else currentLoadingPageKey - 1

        // 返回分页结果,并填入前一页的 key 和后一页的 key
        return LoadResult.Page(
            data = response.data,
            prevKey = prevKey,
            nextKey = currentLoadingPageKey.plus(1)
        )
     catch (e: Exception) 
        return LoadResult.Error(e)
    

上面例子是一个自定义的数据源, Paging2 数据源中 load 相关的 API 有多个,但是 Paging3 中都统一成唯一的 load 方法,我们通过 LoadParams 获取分页请求的参数信息,并根据请求结果的成功与否,返回 LoadResult.Page() ,LoadResult.Invalid 或者 LoadResult.Error,方法的的输入输出都十分容理解。

支持 RxJava 等主流三方库

在 Paging3 中我们通过 Pager 类订阅分页请求的结果,Pager 内部请求 PagingSource 返回的数据,可以使用 Flow 返回一个可订阅结果

class MainViewModel(private val apiService: APIService) : ViewModel() 
        val listData = Pager(PagingConfig(pageSize = 6)) 
                    PostDataSource(apiService)
        .flow.cachedIn(viewModelScope)

除了默认集成的 Flow 方式以外,通过扩展 Pager 也可返回 RxJava,Guava 等其他可订阅类型

implementation "androidx.paging:paging-rxjava2:$paging_version"
implementation "androidx.paging:paging-guava:$paging_version"

例如,paging-rxjava2 中提供了将 Pager 转成 Observable 的方法:

val <Key : Any, Value : Any> Pager<Key, Value>.observable: Observable<PagingData<Value>>
    get() = flow.conflate().asObservable()

新增的事件监听

Paging3 通过 PagingDataDiffer 检查列表数据是否有变动,如果提交数据与并无变化则 PagingDataAdapter 并不会刷新视图。 因此 Paging3 为 PagingDataDiffer 中新增了 addOnPagesUpdatedListener 方法,通过它可以监听提交数据是否确实更新到了屏幕。

配合 Room 请求本地数据源

通过 room-paging ,Paging3 可以配合 Room 实现本地数据源的分页加载

implementation "androidx.room:room-paging:2.5.0-alpha01"

room-paging 提供了一个开箱即用的数据源 LimitOffsetPagingSource

/**
 * An implementation of [PagingSource] to perform a LIMIT OFFSET query
 *
 * This class is used for Paging3 to perform Query and RawQuery in Room to return a PagingSource
 * for Pager's consumption. Registers observers on tables lazily and automatically invalidates
 * itself when data changes.
 */
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
abstract class LimitOffsetPagingSource<Value : Any>(
    private val sourceQuery: RoomSQLiteQuery,
    private val db: RoomDatabase,
    vararg tables: String,
) : PagingSource<Int, Value>() 

在构造时,基于 SQL 语句创建 RoomSQLiteQuery 并连同 db 实例一起传入即可。

更多参考:https://proandroiddev.com/paging-3-easier-way-to-pagination-part-1-584cad1f4f61

1.3 Navigation 2.4

Multiple back stacks 多返回栈

Navigation 2.4.0 增加了对多返回栈的支持。当下大部分移动应用都带有多 Tab 页的设计。由于所有 Tab 页共享同一个 NavHostFramgent 返回栈,因此 Tab 页内的页面跳转状态会因 Tab 页的切换而丢失,想要避免此问题必须创建多个 NavHostFragment。

implementation "androidx.navigation:navigation-ui:$nav_version"

在 2.4 中通过 navigation-ui 提供的 Tab 页相关组件,可以实现单一 NavHostFragment 的多返回栈

class MainActivity : AppCompatActivity() 

    private lateinit var navController: NavController
    private lateinit var appBarConfiguration: AppBarConfiguration

    override fun onCreate(savedInstanceState: Bundle?) 
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val navHostFragment = supportFragmentManager.findFragmentById(
            R.id.nav_host_container
        ) as NavHostFragment
        //获取 navController
        navController = navHostFragment.navController

        // 底部导航栏设置 navController
        val bottomNavigationView = findViewById<BottomNavigationView>(R.id.bottom_nav)
        bottomNavigationView.setupWithNavController(navController)

        // AppBar 设置 navController
        appBarConfiguration = AppBarConfiguration(
            setOf(R.id.titleScreen, R.id.leaderboard,  R.id.register)
        )
        val toolbar = findViewById<Toolbar>(R.id.toolbar)
        setSupportActionBar(toolbar)
        toolbar.setupWithNavController(navController, appBarConfiguration)
    

    override fun onSupportNavigateUp(): Boolean 
        return navController.navigateUp(appBarConfiguration)
    

如上,通过 navigation-ui 的 setupWithNavController 为 BottomNavigationView 或者 AppBar 设置 NavController,当 Tab 页来回切换时依然可以保持 Tab 内部的返回栈状态。升级到 2.4.0 即可,无需其他代码上的修改。

更多参考:https://medium.com/androiddevelopers/navigation-multiple-back-stacks-6c67ba41952f

Two pane layout 双窗格布局

在平板等大屏设备下,为应用采用双窗格布局将极大提升用户的使用体验,比较典型的场景就是左屏列展示表页,右屏展示点击后的详情页。SlidingPaneLayout 可以为开发者提供这种水平的双窗格布局

Navigation 2.4.0 提供了AbstractListDetailFragment,内部通过继承 SlidingPaneLayout ,实现两侧 Fragment 单独显示,而详情页部分更是可以实现独立的页面跳转:

class TwoPaneFragment : AbstractListDetailFragment() 

    override fun onCreateListPaneView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View 
        return inflater.inflate(R.layout.list_pane, container, false)
    

    //创建详情页区域的 NavHost
    override fun onCreateDetailPaneNavHostFragment(): NavHostFragment 
        return NavHostFragment.create(R.navigation.two_pane_navigation)
    

    override fun onListPaneViewCreated(view: View, savedInstanceState: Bundle?) 
        super.onListPaneViewCreated(view, savedInstanceState)
        val recyclerView = view as RecyclerView
        recyclerView.adapter = TwoPaneAdapter(map.keys.toTypedArray()) 
            map[it]?.let  destId -> openDetails(destId) 
        
    

    private fun openDetails(destinationId: Int) 
        //获取详情页区域的 NavController 实现详情页的内容切换
        val detailNavController = detailPaneNavHostFragment.navController
        detailNavController.navigate(
            destinationId,
            null,
            NavOptions.Builder()
                .setPopUpTo(detailNavController.graph.startDestinationId, true)
                .apply 
                    if (slidingPaneLayout.isOpen) 
                        setEnterAnim(R.anim.nav_default_enter_anim)
                        setExitAnim(R.anim.nav_default_exit_anim)
                    
                
                .build()
        )
        slidingPaneLayout.open()
    

    companion object 
        val map = mapOf(
            "first" to R.id.first_fragment,
            "second" to R.id.second_fragment,
            "third" to R.id.third_fragment,
            "fourth" to R.id.fourth_fragment,
            "fifth" to R.id.fifth_fragment
        )
    

支持 Compose

Navigation 通过 navigation-compose 支持了 Compose 的页面导航,这对于一个 Compose first 的项目非常重要。

implementation "androidx.navigation:navigation-compose:$nav_version"

navigation-compose 中,Composable 函数替代 Fragment 成为页面导航的 Destination,我们使用 DSL 定义基于 Composable 的 NavGraph:

val navController = rememberNavController()
Scaffold  innerPadding ->
    NavHost(navController, "home", Modifier.padding(innerPadding)) 
        composable("home") 
            // This content fills the area provided to the NavHost
            HomeScreen()
        
        dialog("detail_dialog") 
            // This content will be automatically added to a Dialog() composable
            // and appear above the HomeScreen or other composable destinations
            DetailDialogContent()
        
    

如上, composable 方法配置导航中的 Composable 页面,dialog 配置对话框,而 navigation-fragment 中各种常见功能,比如 Deeplinks,NavArgs,甚至对 ViewModel 的支持在 Compose 项目中同样可以使用。

1.4 Fragment

每次 I/O 大会几乎都有关于 Fragment 的分享,因为它是我们日常开发中重度使用的工具。本次大会没有带来 Fragment 的新功能,相反对 Framgent 的功能进行了大幅“削减”。不必惊慌,这并非是从代码上删减了功能,而是对 Fragment 使用方式的重定义。随着 Jetpack 组件库的丰富,Fragment 的很多职责已经被其他组件所分担,所以谷歌希望开发者能够重新认识这个老朋友,对使用场景的必要性进行更合理评估。

Fragmen 在最早的设计中作为 Activity 的代理者出现,因此它承担了很多来自 Activity 回调,例如 Lifecycle,SaveInstanceState,onActivityResult 等等

以前:各种职责现在:职责外移

而如今这些功能已经有了更好的替代方案,生命周期可以提供 Lifecycle 组件感知,数据的保存恢复也可以通过 ViewModel 实现,因此 Fragment 只需要作为页面侧承载着持有 View 即可,而随着 Navigation 对 Compose 的支持,Fragment 作为页面载体的职责也变得不在必要。

尽管如此,我们也并不能彻底抛弃 Fragment,在很多场景中 Fragment 仍然是最佳选择,比如我们可以借助它的 ResultAPI 实现更简单的跨页面通信:

当我们需要通知一些一次性结果时,ResulAPI 比共享 ViewModel 的通信方式将更加简单安全,它像普通回调一般的使用方式极其简单:

// 在 FramgentA 中监听结果
setFragmentResultListener("requestKey")  requestKey, bundle ->
    // 通过约定的 key 获取结果
    val result = bundle.getString("bundleKey")
    // ...

    
// FagmentB 中返回结果
button.setOnClickListener 
    val result = "result"
    // 使用约定的 key 发送结果
    setFragmentResult("requestKey", bundleOf("bundleKey" to result))
 

总结起来,Fragment 仍然是我们日常开发中的重要手段,但是它的角色正在发生变化。

2. Performance

2.1 JankStats 卡顿检测

JankStats 用来追踪和分析应用性能,发现 Jank 卡顿问题,它最低向下兼容到 API 16,可以在绝大多数机器设备上使用,有了它我们不必再求助 BlockCanery 等三方工具了。

implementation "androidx.metrics:metrics-performance:1.0.0-alpha01"

我们需要为每个 Window 创建一个 JankStats 实例,并通过 OnFrameListener 回调获取包含是否卡顿在内的帧信息,示例如下:

class JankLoggingActivity : AppCompatActivity() 

    private lateinit var jankStats: JankStats

    override fun onCreate(savedInstanceState: Bundle?) 
        super.onCreate(savedInstanceState)
        // ...
        // metricsStateHolder可以收集环境信息,跟随帧信息返回
        val metricsStateHolder = PerformanceMetricsState.getForHierarchy(binding.root)

        // 基于当前 Window 创建 JankStats 实例
        jankStats = JankStats.createAndTrack(
            window,
            Dispatchers.Default.asExecutor(),
            jankFrameListener,
        )

        // 设置 Activity 名字到环境信息
        metricsStateHolder.state?.addState("Activity", javaClass.simpleName)
        // ...
    

    private val jankFrameListener = JankStats.OnFrameListener  frameData ->
        // 监听到的帧信息
        Log.v("JankStatsSample", frameData.toString())
    

PerformanceMetricsState 用来收集你希望跟随 frameData 一起返回的状态信息,比如上面例子中设置了当前 Activity 名称,下面是 frameData 的打印日志:

JankStats.OnFrameListener: FrameData(frameStartNanos=827233150542009, frameDurationUiNanos=27779985, frameDurationCpuNanos=31296985, isJank=false, states=[Activity: JankLoggingActivity])

更多参考:https://medium.com/androiddevelopers/jankstats-goes-alpha-8aff942255d5

2.2 Baseline Profiles 基准配置

Android 8.0 之后默认开启 ART 虚拟机。ART 最初版本在安装应用时会对全部代码进行 AOT 预编译,将字节码转换为机器码存在本地,这提升了运行时的速度,但是会导致安装过程变慢。因此后来 ART 改进为 JIT 和 AOT 相结合的方式,在应用安装时只将热点代码编译成机器码,缩短安装时间。

Baselin Profiles 基准配置文件允许我们配置哪些代码成为热点代码。基准配置文件将在 APK 的 assets/dexopt/baseline.prof 中编译为二进制形式,例如如果我们想提升首帧的性能,可以将应用启动或帧渲染期间使用的方法配置到 prof 文件中。

prof 文件可以通过自动或手动方式生成,我们可以编写 JUnit4 测试用例,通过执行 BaselineProfileRule 在测试中发现待优化的瓶颈代码,并生成对应的 prof 文件

@ExperimentalBaselineProfilesApi
@RunWith(AndroidJUnit4::class)
class BaselineProfileGenerator 
    @get:Rule val baselineProfileRule = BaselineProfileRule()

    @Test
    fun startup() =
        baselineProfileRule.collectBaselineProfile(packageName = "com.example.app") 
            pressHome()
            startActivityAndWait()
        

我们也可以手动创建 prof 文件,只需遵循一些简单的语法规则。例如下面展示了 Jetpack Compose 库中包含的一些 Prof 规则,

HSPLandroidx/compose/runtime/ComposerImpl;->updateValue(Ljava/lang/Object;)V
HSPLandroidx/compose/runtime/ComposerImpl;->updatedNodeCount(I)I
HLandroidx/compose/runtime/ComposerImpl;->validateNodeExpected()V
PLandroidx/compose/runtime/CompositionImpl;->applyChanges()V
HLandroidx/compose/runtime/ComposerKt;->findLocation(Ljava/util/List;I)I
Landroidx/compose/runtime/ComposerImpl;

上述配置遵循 [FLAGS][CLASS_DESCRIPTOR]->[METHOD_SIGNATURE] 格式,其中 FLAGS 中的 H/S/P 代表方法的调用实际,比如是否是启动时调用等。

更多参考:https://android-developers.googleblog.com/2022/01/improving-app-performance-with-baseline.html

2.3 Benchmark 基准测试

Jetpack 当前提供了两套 Benchmark 库,Microbenchmark 和 Macrobenchmark (微基准和宏基准),分别用于不同场景下的基准测试。

Mircobenchmark 的测试对象是代码块,它的依赖如下:

androidTestImplementation 'androidx.benchmark:benchmark-junit4:1.1.0-beta03'

我们可以在 JUnit4 中应用 BenchmarkRule,示例如下:

@RunWith(AndroidJUnit4::class)
class SampleBenchmark 
    @get:Rule
    val benchmarkRule = BenchmarkRule()

    @Test
    fun benchmarkSomeWork() 
        benchmarkRule.measureRepeated 
            doSomeWork() //执行待测试代码
        
    

Macrobenchmark 通常面向更大粒度的场景测试,例如一个 Activity 启动或者一个用户操作等。由于 Macrobenchmark 不进行代码级别测试,我们可以创建独立于业务代码的单独模块进行测试:

下面展示了使用 MacrobenchmarkRule 测试一个 Activity 的启动:

    @get:Rule
    val benchmarkRule = MacrobenchmarkRule()

    @Test
    fun startup() = benchmarkRule.measureRepeated(
        packageName = "mypackage.myapp",
        metrics = listOf(StartupTimingMetric()),
        iterations = 5,
        startupMode = StartupMode.COLD
    )  // this = MacrobenchmarkScope
        pressHome()
        val intent = Intent()
        intent.setPackage("mypackage.myapp")
        intent.setAction("mypackage.myapp.myaction")
        startActivityAndWait(intent)
    

配合 2021.1.1 或更高版本的 Android Studio ,Benchmark 的测试结果会直接显示在 IDE 窗口中。

当然,测试结果也可以导出为 JSON 格式

更多参考:https://medium.com/androiddevelopers/measure-and-improve-performance-with-macrobenchmark-560abd0aa5bb

2.4 Tracing 事件追踪

Tracing 用来在代码添加 trace 信息,trace 信息可以显示在 Systrace 和 Perfetto 等工具中。

implementation "androidx.tracing:tracing:1.1.0-beta01"

下面的例子汇总,我们通过 Trace 类的 benginSection/endSection 方法追踪 onCreateViewHolder 和 onBindViewHolder 方法执行的起始点

class MyAdapter : RecyclerView.Adapter<MyViewHolder>() 
    override fun onCreateViewHolder(parent: ViewGroup,
            viewType: Int): MyViewHolder 
        return try 
            Trace.beginSection("MyAdapter.onCreateViewHolder")
            MyViewHolder.newInstance(parent)
         finally 
            //endSection 放到 finally 里,当出现异常时也会调用
            Trace.endSection()
        
    

    override fun onBindViewHolder(holder: MyViewHolder, position: Int) 
        Trace.beginSection("MyAdapter.onBindViewHolder")
        try 
            try 
                Trace.beginSection("MyAdapter.queryDatabase")
                val rowItem = queryDatabase(position)
                dataset.add(rowItem)
             finally 
                Trace.endSection()
            
            holder.bind(dataset[position])
         finally 
            Trace.endSection()
        
    

需要注意 benginSection/endSection 必须成对出现,且必须在同一线程中。我们 Trace 的 section 会作为新增的自定义事件出现在 Perfetto 等工具视图中:

3. UI

3.1 WindowManager

这并非系统 WMS 获取的那个 WindowManager,它是 Jetpack 的新成员,当前刚刚迈入 1.1.0。

implementation "androidx.window:window:1.1.0-alpha02"

它可以帮助我们适配日益增多的可折叠设备,满足多窗口环境下的开发需求。
可折叠设备通常分为两类:单屏可折叠设备(一个整体的柔性屏幕)和双屏可折叠设备(两个屏幕由合页相连)。

目前单屏可折叠设备正逐渐成为主流,但无论哪种设备都可以通过 WindowManager 感知当前的屏幕显示特性,例如当前折叠的状态和姿势等。

获取折叠状态

多屏设备下,一个窗口可能会跨越物理屏幕显示,这样窗口中会出现铰链等不连续部分,FoldingFeature (DisplayFeature 的子类)对铰链这类的物理部件进行抽象,从中可以获取铰链在窗口中的准确位置,帮助我们避免将关键交互按钮布局在其中。另外 FoldingFeature 还提供了可以感知感知当前折叠状态的 API,我们可以根据这

以上是关于谷歌 I/O 深度解析:Android Jetpack 最新变化的主要内容,如果未能解决你的问题,请参考以下文章

深度 | 谷歌I/O走进TensorFlow开源模型世界:从图像识别到语义理解

谷歌I/O 2020开发者大会如期举行 Android 11备受期待

超深度解析 Linux I/O 的那些事儿

线上也不办了,谷歌宣布取消I/O开发者大会,Android 11怎么办?

从TPU3.0到DeepMind支持的Android P,谷歌I/O 2018的AI亮点全在这了

Android深度探索第七章