Android的UI框架学习日记(1)

155 阅读8分钟

Android的UI框架学习日记(1)

本文主要分析第一个示例App的工程结构


1. 背景

作为Android工程架构的一员,我主要负责Gradle构建体系的设计与维护。然而,作为一名偏底层工具开发者,我发现自己在实际业务开发方面仍有较大空白,甚至尚未完整编写过一个标准的Activity。

本系列文章将尝试以尽可能底层的视角,系统性还原Android应用从启动到呈现的全链路过程。

2. IDEA搭建工程

用AS直接搭建即可。

setting.gradle

pluginManagement {
    repositories {
        google {
            content {
                includeGroupByRegex("com\.android.*")
                includeGroupByRegex("com\.google.*")
                includeGroupByRegex("androidx.*")
            }
        }
        mavenCentral()
        gradlePluginPortal()
    }
}
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
    }
}
​
rootProject.name = "appdemo"
include ':app'
​

因为公司项目使用的gradle文件的编写方式比较老,导入插件普遍采用类似这样的方法,对新版本的管理方式比较陌生。

buildscript {
  classpath "XX"
}
apply plugin: "xx"

所以这里也聊一下这种 pluginManagement 管理的逻辑。

他实际上相当于让每个 plugin 有自己单独的解析的逻辑,所以不用我们再管 classpath 了。外部的设置好 repositories , 各个子module下面直接使用即可。

plugins {
  id "XX" version "YY"
}

这个写法很早就引入了gradle,但是没什么太明显的优点,而且对于大型工程来说,buildscript本来就肯定要写的,所以我们还是坚持buildscript加apply的写法。我个人觉得这样的写法看起来更可预测,所以我也偏爱老写法。

dependencyResolutionManagement 是用来管理统一dependencies的, 这样我们不再需要在每个子项目中再次写这个:

repositories {
  maven {
    url "XX"
  }
}

但我个人感觉这样的管理模式不够有吸引力,还让工程看起来更复杂了,对每个module单独配置更灵活,也好理解。

build.gradle

// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
alias(libs.plugins.android.application) apply false
    alias(libs.plugins.kotlin.android) apply false
    alias(libs.plugins.kotlin.compose) apply false
}

这个文件配合gradle/libs.versions.toml

[versions]
agp = "8.8.1"
kotlin = "2.0.0"
coreKtx = "1.16.0"
junit = "4.13.2"
junitVersion = "1.2.1"
espressoCore = "3.6.1"
lifecycleRuntimeKtx = "2.8.7"
activityCompose = "1.10.1"
composeBom = "2024.04.01"[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }
androidx-compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" }
androidx-ui = { group = "androidx.compose.ui", name = "ui" }
androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" }
androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" }
androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" }
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
​
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }

就是用了别名, 然后不用在build.gradle里指定版本, 因为toml里都写好了。

如果能理解原来的管理模式,理解这种新的就不算困难,就是换了一套管理的逻辑,本质还是需要依赖的group name version等属性。然后规定从哪里获取它。

管理的底层源码目前不打算深究, 之后可能会写文档来分析。

app/build.gradle

// 本模块需要用到的plugin
plugins {
    alias(libs.plugins.android.application)
    alias(libs.plugins.kotlin.android)
    alias(libs.plugins.kotlin.compose)
}
​
​
// 对AGP的配置
android {
    namespace 'com.example.appdemo'
    // 这里是在表达我源码中android API可以使用35的接口
    compileSdk 35
​
    defaultConfig {
        applicationId "com.example.appdemo"
        // 要安装我的引用,你的android系统版本至少要是24(只让安装,不保证能跑)
        minSdk 24
        // 跑应用的时候android会读取这个数值,决定使用的行为变更 targetSdk <= compileSdk
        // targetSdk如果比compileSdk大,说明你在看不到新版SDK的情况下说自己做了适配,不合理
        targetSdk 35
        versionCode 1
        versionName "1.0"
​
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    
    buildTypes {
        release {
            // 这里是true或者false会决定是否开启R8
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
  
    compileOptions {
        // 非常实用的配置
        // source用来表征“我写的代码会用到什么版本的特性”
        // target用来表征“我希望我写的代码会被编译成什么版本的字节码”
        // 这里随便写不会报错, 但是需要注意生成的target字节码和目标机器的Java接口的兼容性。
        // 比如这里我的代码使用了Java11才有的方法isBlank(), 然后编译成了Java8兼容的字节码
        // 如果运行时环境时Java11就还是没问题, 但如果是Java8就会报错,java.lang.NoSuchMethodError
        sourceCompatibility JavaVersion.VERSION_11
        targetCompatibility JavaVersion.VERSION_1_8
    }
    // kotlin编译器的设置(Android非常推荐使用Koltin替代Java)
    // 注意这里的目标必须和targetCompatibility一致
    kotlinOptions {
        jvmTarget = '1.8'
    }
    
    // 用来开启AGP的一些构建特性,比如这里就开启了JetPack Compose
    buildFeatures {
        compose true
    }
}
​
dependencies {
    //主依赖
    implementation libs.androidx.core.ktx
    implementation libs.androidx.lifecycle.runtime.ktx
    implementation libs.androidx.activity.compose
    implementation platform(libs.androidx.compose.bom)
    implementation libs.androidx.ui
    implementation libs.androidx.ui.graphics
    implementation libs.androidx.ui.tooling.preview
    implementation libs.androidx.material3
  
    // src/test下的代码会用的依赖
    testImplementation libs.junit
  
    //src/androidTest
    androidTestImplementation libs.androidx.junit
    androidTestImplementation libs.androidx.espresso.core
    androidTestImplementation platform(libs.androidx.compose.bom)
    androidTestImplementation libs.androidx.ui.test.junit4
  
    // buildType是debug的时候会用的依赖
    debugImplementation libs.androidx.ui.tooling
    debugImplementation libs.androidx.ui.test.manifest
​
    implementation("androidx.appcompat:appcompat:1.6.1")
​
}

3. UI框架选型

目前android主流的两种UI开发方法

3.1 XML布局 + Java/Koltin控制逻辑

我们通常在XML里写好View,然后在代码中访问。

我们以一个简单的XML文件为例,说一说XML文件的结构:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
​
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!" />
</LinearLayout>

第一行指明了当前文件的xml标准以及编码方式

<?xml version="1.0" encoding="utf-8"?>
// 这里的xmlns:android="http://schemas.android.com/apk/res/android"是命名空间
// LinearLayout是一个容器, 后面的android:XX=YY表明了他的属性,vertical表示他是竖直布局
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
      // 这是其中的一个View
      <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!" />
</LinearLayout>

到此一个基本的用XML描述的界面我们就写好了。

现在问题变成了这个描述界面是如何在android上面跑起来的

从流程上讲,我们的的xml应该会和我们的源码一起打包到package。这个xml是界面,所以他必须放在res/layout下

因为xml不一定是负责描述UI的,他在android工程里有很多作用。我们放在哪个目录下,决定了aapt会怎么看待我们的xml,所以这里是强制要求而不是建议

这个xml会被编译到R.layout下,所以我们可以在代码里使用它,类似:

package com.example.appdemo
​
import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
​
class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }
}

在Android开发中,有一个看似矛盾但非常有趣的现象:我们经常直接通过R.layout.xx的方式引用资源,而并未显式import R类,且编译器不会报错。这背后的本质在于,R类虽然是由构建工具aapt或aapt2生成的,但编译器和IDE对它进行了特殊处理。

首先,R类位于当前模块的包名下,例如com.example.appdemo.R。由于它与MainActivity处于同一包,编译器允许直接访问而无需显式导入。当然,开发者也可以选择显式导入,它本质上就是一个普通的final类。

更有意思的是,在Android Studio中,我们可以在创建XML布局文件后,立即使用R.layout.new_layout而IDE不会报错,哪怕此时还未触发真正的Gradle构建任务。这是因为Android Studio并不依赖实际生成的R.java或R.class文件,而是通过资源索引系统对res目录进行实时扫描,并构建内存中的资源符号模型。该模型使得IDE能够提供代码补全、跳转、静态分析等能力,即便底层还未真正生成对应的R字节码。

因此,我们在开发阶段所见的R.layout.xx更多是由IDE的索引系统支撑,而非真实存在的class文件。是否报错的关键,并不取决于R的物理存在,而是IDE是否成功解析了资源索引与符号映射。

另一个经常看见网上有博客告诉你你在哪里可以看到R.java。实际上现在已经不会显式的生成R.java了,而是生成class。

事实上,R.class 会在 compileDebugJavaWithJavac这个task中生成,我们可以在build/intermediates/compile_and_runtime_not_namespaced_r_class_jar/debug/processDebugResources/R.jar找到

./gradlew compileDebugJavaWithJavac --info

至于添加一个按钮,findViewById 这种暂时就不再赘述。

3.2 Compose

Compose目前是Android官方主推的编程方式,用AS新创建一个Android工程也是默认采用了Compose框架。

Compose几乎摈弃了XML来设计UI这种模式,可以直接在源码里写控制逻辑和UI样式。

我们使用gradle构建的工程,这个地方就是compose的开关。

android {
  buildFeatures {
    // 这里设置为True
    compose true
  }
}

所以实际上要启用compose,我们需要做两件事

  • 引入依赖(implementation XX)
  • 开启Compose支持(在AGP里面设置)

这里我们就发现了这个框架的特别之处。因为我们发现使用这个框架竟然不是仅仅导入依赖就可以了,还要设置AGP的extension。

这和Compose的底层实现有关。他和XML的写法完全走的是不同的渲染逻辑,比较复杂。

这里的设置实际上和kotlin编译器有关,会注册一些行为,比如识别@Composable这样的注解,这里不再深入展开。

这里展示一段示例代码

package com.bytedance.appdemo
​
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.bytedance.appdemo.ui.theme.AppDemoComposeTheme
​
// 这里不再是AppCompactActivity而是ComponentActivity,他是compose框架的Activity
class MainActivity : ComponentActivity() {
    // Bundle是Activity的状态, 用于它优雅的重建
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 沉浸界面,可以让你的界面内容在状态栏和导航栏下面
        enableEdgeToEdge()
        // compose的入口点, 通过lambda实现类似HTML的嵌套结构
        setContent {
            // 自动生成的类,统一颜色、字体等UI样式
            AppDemoComposeTheme {
                // 页面结构容器
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    // 组件
                    Greeting(
                        name = "Android",
                        modifier = Modifier.padding(innerPadding)
                    )
                }
            }
        }
    }
}
​
//用@Composable标记,组件实现
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Text(
        text = "Hello $name!",
        modifier = modifier
    )
}
​
//与App无关,用于IDE预览UI, 点击右上角split可以看到预览图
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    AppDemoComposeTheme {
        Greeting("Android")
    }
}