Appearance
北工大安卓开发
更新: 12/9/2025 字数: 0 字 时长: 0 分钟
未完成的后续内容可以参考某高手的,已经完成的内容也可以做个互补:某个个人博客里 GitHub 连接指的是 Vitepress 而不是自己 profile 的神人的笔记
开发速览
安卓系统从上到下分为四层(Android Software Stack):
Applications (应用层)
就是手机上装的各种 APP。
Application Framework (应用框架层)
提供了我们开发 APP 时需要调用的各种系统服务的 API,比如 Activity Manager, Window Manager, Content Providers 等。
Libraries and Android Runtime (库和安卓运行时)
包含一系列用 C/C++ 写的库,来处理图形、媒体、数据库等高性能任务。这就是 Java/Kotlin 代码能跑起来的地方(早前 Dalvik,现 ART)。
在 Android 7.0 之前所基于的 Java 实现是 Apache Harmony (现已弃用),之后改为 OpenJDK 。
INFO
ART (Android Runtime) 是 Dalvik 虚拟机的继任者,使用预编译技术 (Ahead-of-Time, AOT) ,它在应用安装时就把应用完全编译成本地机器码,换来了更高的性能但需要更多的存储空间。
不过,ART 运行的仍是
.dex文件,即 Dalvik Executable 格式的字节码文件。Linux Kernel (Linux 内核)
安卓是基于 Linux 内核的。
由于移动设备资源非常有限,安卓开发非常关注性能和内存管理。
这个资源有限的考量,是后续所有复杂知识点(比如 Service、BroadcastReceiver、Activity生命周期)的都有在关注的根本。
四大组件 (Building Blocks)
Activities (活动)
这是构成用户界面的基础。
可以视作应用里的“一个屏幕”或“一个窗口” 。用户能看到和交互的一切,都发生在 Activity 里。
Services (服务)
这是一个在后台运行的组件。
当你用别的 APP 时,音乐播放器在后台依然能工作,这就是 Service 在发挥 作用。
Content Providers (内容提供者)
它为多个应用提供了一个抽象的数据访问层。
Content Provider 用来安全地在应用之间共享数据的机制(比如让别的 app 访问日历)。
Intents (意图)
Intent 是系统消息,在设备内部运行,用于向应用程序通知各类事件
你想从一个屏幕(Activity A)跳转到另一个屏幕(Activity B),就需要发送一个 Intent 。你想打开相机拍照,也是发送一个 Intent。
项目文件结构
安卓开发的项目结构规范中有3个重要文件:
AndroidManifest.xml(清单文件)该清单文件是整个应用的地基,功能包括但不限于:
- 列出了应用所有的 Activity 和 Service。
- 声明应用需要的权限(permission),比如访问网络、读取联系人 。
- 声明应用的图标 (
android:icon) 和名称 (android:label) 。 - 指定启动 Activity :通过
<intent-filter>里的标签android.intent.action.MAIN,指定哪个 Activity 是点APP 图标时第一个打开的。
java/或src/目录存放所有的
.java或.kotlin源代码文件。res/目录存放所有非代码的静态资源。
子目录 说明 drawable/存放图片资源 layout/存放界面布局文件(XML 格式) values/存放字符串( strings.xml)、颜色(colors.xml)、样式(styles.xml),尺寸值(dimens.xml)等资源文件mipmap/存放应用图标的不同分辨率版本 Android Studio 会自动生成一个
R.java文件,里面包含了所有资源的 ID,方便在代码中引用。 ex:R.layout.activity_main->res/layout/activity_main.xml。
连接代码与布局 (XML)
布局文件 (
activity_main.xml) - 位于/res/layout/用来定义界面的外观和结构。
它使用各种标签(View),比如
<TextView>(显示文本) 和<RelativeLayout>(一种布局方式) 。和所有控件一样,它有两个非常重要的属性:
android:layout_width和android:layout_height。值
match_parent:表示和父容器一样大。值
wrap_content:表示刚刚好能包住里面的内容。
Activity 代码 (
MainActivity.java或MainActivity.kt)这是项目的入口,默认启动的 Activity,负责加载布局文件并处理用户交互,你应当把其用作 app 的 Controller。
继承自
AppCompatActivity。在 Activity 创建时
onCreate()方法自动被调用,其中最重要的一行是:javasetContentView(R.layout.activity_main); // 在 kotlin 中写法基本一致这行代码会去加载
R.java文件里 ID 为layout.activity_main的那个资源(即res/layout/activity_main.xml),并把它作为这个 Activity 的界面显示出来。
布局文件速览
这是用来绘制界面的 XML 布局文件,由嵌套的元素组成。
示例
xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>常用元素:
| 组件 | 作用 | 关键属性(代码示例) |
|---|---|---|
| TextView | 显示一段只读文字 | android:text="你要显示的文字" |
| Button | 可点击的按钮 | android:text="按钮上的文字" |
| ImageView | 显示一张图片 | android:src="@drawable/molecule" |
| EditText | 输入和编辑文字的输入框 | android:hint="提示文字" |
| CheckBox | 多选框(可选多个) | android:text="复选框旁边的文字" |
| RadioGroup + RadioButton | 单选框(只能选一个,需配合使用) | android:orientation="vertical"(RadioGroup) |
所有 View 的通用属性:
xml
android:id="@+id/xxx" // 视图的唯一标识符
android:layout_width="..." // 视图的宽度
android:layout_height="..." // 视图的高度
android:padding="..." // 视图内容与边界的间距
android:layout_margin="..." // 视图与其他视图的间距
android:visibility="..." // 视图的可见性 (visible, invisible, gone)令 Button 监听一个事件:
java
Button button = findViewById(R.id.button);
// 可使用 Lambda 表达式替代
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 处理按钮点击事件
}
});Activities
Activity 的生命周期
由于手机内存和电量有限,当你的 App 退到后台,系统可能会为了省内存而“杀死”它。了解 Activity 生命周期可以帮助你在 app 被 “杀死”之前保存工作,在回来时又能恢复。
核心状态:
| Active | 在前台,用户能看到和交互 |
| Paused | 仍然可见,但被一个弹窗或通知挡住了一部分 |
| Stopped | 完全被其他 Activity 遮挡,退到后台了 |
| Inactive/dead | 从来没打开过app,或被系统杀死,内存被回收 |
null
核心回调
许多重要回调都是成双成对的,在相反的时机被自动调用。
| 回调 | 说明 |
|---|---|
| 创建/销毁 | - |
onCreate() | Activity 第一次被创建时调用。 这是你设置布局 (setContentView) 和初始化数据的地方 。 |
onDestroy() | Activity 被销毁时调用。用来清理所有资源 (比如数据库连接) 。 |
| 前台/后台 | - |
onResume() | Activity 进入前台,可以交互时调用。 |
onPause() | Activity 失去前台焦点时调用 (比如来了个弹窗)。 该方法保证会被调用,适合用来保存持久化数据。 |
| 可见/不可见 | - |
onStart() | Activity 变得对用户可见时调用。 |
onStop() | Activity 不再对用户可见时调用(ex:按了 Home 键)。 |
如果用户只是旋转屏幕,或者 App 在后台被系统回收了,onCreate() 会被重新调用,但所有数据(比如输入框里的字)都会丢失,因此需要一种保存临时状态的方法。
保存临时数据的方法有二:
onSaveInstanceState(Bundle outState):在 Activity 可能被销毁前调用。你用它来保存临时状态(比如输入框的文字)。onCreate(Bundle savedInstanceState):在 onCreate() 里,你可以通过检查savedInstanceState这个参数是否为 null,来判断是全新启动还是从保存的状态恢复。
Fragments
Fragment 是一个可重用的子 Activity。你可以把它想象成一个可以嵌入到 Activity 里的小界面。你可以在多个 Activity 里重用同一个 Fragment。
Fragment 特点在于强大的灵活性,在处理不同屏幕尺寸和方向的布局上非常方便。
创建 Fragment
你的类首先要继承自 Fragment 类,然后实现以下方法:
onCreate():Fragment 附加到 Activity 时调用。onCreateView():会在onCreate()之后调用,你需要在该方法里加载 Fragment 的布局文件并返回一个 View 对象。onPause():当 Fragment 不再处于前台时调用,可以用于保存需要持久化的数据。
Fragment 的生命周期
Fragment 拥有自己的生命周期,同时依附于宿主 Activity 的生命周期。
null
Fragment 的布局 XML
Fragment 的界面也是由一个单独在 res/layout/ 下的 XML 文件定义,语法也和 Activity 的布局文件一致。
xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".BlankFragment">
<!-- TODO: Update blank fragment layout -->
<TextView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="@string/hello_blank_fragment" />
</FrameLayout>要将 XML 和 Fragment 类关联起来,需要在 onCreateView() 方法中使用 inflater.inflate() 来加载这个 XML 布局文件,并把它转换成一个 View 对象返回给系统。例如:
java
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// "R.layout.example_fragment" 就是这个 Fragment 自己的 XML 布局文件
return inflater.inflate(R.layout.example_fragment, container, false);
}把 Fragment 放进 Activity 的布局里
有两种方法可以把 Fragment 放进 Activity 里。
静态添加 (在 XML 里定义)
你可以直接在 Activity 的布局 XML 里使用
<fragment>标签来像添加一个 View 一样,使用<fragment>标签来添加 Fragment。例如:xml<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="horizontal"> <fragment android:name="com.example.news.ArticleListFragment" android:id="@+id/list" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="1" /> <fragment android:name="com.example.news.ArticleReaderFragment" android:id="@+id/viewer" android:layout_width="0dp" android:layout_height="match_parent" android:layout_weight="2" /> </LinearLayout>android:name:用来指定加载的 Fragment 类。android:id:指定此 Fragment 的唯一 ID。
在代码里动态添加
这是更灵活、更常用的方法,用于在运行时切换 Fragment。
在 Activity 的布局 XML 中,你需要预留一个空的占位符容器,如
<FrameLayout>。xml<FrameLayout android:id="@+id/fragment_container" android:layout_width="match_parent" android:layout_height="match_parent" />然后在 在 Activity 的 Java/Kotlin 代码中,你通过
FragmentManager来执行一个FragmentTransaction(片段事务),把你的 Fragment 添加 (add) 或 替换 (replace) 到那个容器里。例如:
java// 创建一个新的 Fragment 实例 Fragment newFragment = new ExampleFragment(); // 开始一个事务 FragmentTransaction transaction = getFragmentManager().beginTransaction(); // 把 fragment_container 容器里的内容替换成 newFragment transaction.replace(R.id.fragment_container, newFragment); transaction.addToBackStack(null); // 提交事务 transaction.commit();
FragmentManager
FragmentManager 的首要职责是管理应用中所有正在运行的 Fragment。在你的 Activity 中,你需要通过调用 getFragmentManager() 方法来获取其实例。
你可以通过 FragmentManager 的 findFragmentById() 或 findFragmentByTag() 方法来查找已经添加的 Fragment 实例。
java
// 获取 FragmentManager 实例
FragmentManager fragmentManager = getFragmentManager();
// 通过 ID 查找 Fragment
Fragment fragment = fragmentManager.findFragmentById(R.id.fragment_container);顺带一提,Tag 是一个字符串标识符,你可以在添加 View 和 Fragment 时通过 .setTag() 指定它,以便后续查找。
FragmentTransaction
FragmentTransaction 则是用来执行对 Fragment 的增删改操作的。你可以通过 FragmentManager 的 beginTransaction() 方法来创建一个新的 FragmentTransaction 实例。
java
// 开始一个 Fragment 事务
FragmentTransaction transaction = fragmentManager.beginTransaction();
// 添加一个新的 Fragment
transaction.add(R.id.fragment_container, new ExampleFragment());
// 提交事务
transaction.commit();除了简单直接的 add() 方法,你还可以替换或移除 Fragment:
java
// 替换 Fragment(最常用)
transaction.replace(R.id.fragment_container, new AnotherFragment());
// 移除 Fragment
transaction.remove(existingFragment);你还可以将一次事务添加到返回栈中,这样用户按返回键时可以撤销这次操作:
java
// 把当前事务添加到返回栈
transaction.addToBackStack(null);返回栈 (Back Stack)
你从 Activity A 跳转到 Activity B,A 就会被压在下面。你再跳转到 Activity C,B 也会被压下去。当你按手机的返回键时,C 会被销毁,B 就会露出来。这就是 Activity 的返回栈 。它是由系统自动管理的。
Fragment 没有这种自动的返回栈。想象一下:
你在
Activity A里,先显示Fragment 1。你点一个按钮,用
Fragment 2替换了Fragment 1。这时,如果你不手动做点什么,
Fragment 1就会被彻底销毁。
当用户按返回键时,系统会以为你想退出 Activity A,整个应用就退出了,这显然不是我们想要的。
Fragment 的返回栈就是为了解决这个问题。它是一个可选的、需要你手动管理的栈,用来保存你执行过的 Fragment 操作。
与 Activity 通信
有时,Fragment 需要告诉 Activity 发生了什么(ex: 用户点击了列表项),或者 Activity 需要告诉 Fragment 去做什么(ex: 刷新你的数据)。
Fragment 如何找到它的宿主 Activity
Fragment 可以通过调用 getActivity() 方法,来获得一个指向宿主 Activity 的引用。
一旦 Fragment 拿到了这个 Activity 对象,它就可以:
- 调用
Activity的 public 方法。 - 使用 Activity 的上下文(Context)。
Activity 如何找到它包含的 Fragment
Activity 需要通过之前谈到的 FragmentManager 来查找已经添加的 Fragment 实例。例如:
java
ExampleFragment fragment = (ExampleFragment) getFragmentManager()
.findFragmentById(R.id.example_fragment);Layouts & View
ViewGroup 是一个容器,定义了它内部的 View 和 ViewGroup 是如何排列的。而 View 则是用户界面的基本构建块,比如按钮、文本框、图像等。
你有两种方式来创建布局,推荐的方式是通过 XML 文件来定义;虽也可以通过代码动态创建(ex: Button btn = new Button(context);),但这是不推荐的做法。
INFO
JetCompose 是一种更新更现代化的 UI 工具包,允许你使用纯 Kotlin 代码来构建界面。但还未流行开来,此处的“通过代码动态创建”并非指 JetCompose。
当你加载一个 XML 布局时,里面的 ViewGroup 和 View 都会被实例化并转化为对应的 Java/Kotlin 对象。
之前已经提到过如何连接 Activity 与布局(
onCreate() { setContentView(...) }),此处不再赘述。
通过代码获取 View 最常用的方法是 findViewById() 方法。例如:
java
// 获取一个按钮
Button btn = (Button) findViewById(R.id.button);
// 获取父布局 - 默认返回类型为 ViewParent
var viewParent = btn.getParent();
// 获取根布局 - 转化为具体类型
View rootView = (ConstraintLayout) btn.getRootView();常见的 View & Layout
| View | 作用 | 关键属性(代码示例) |
|---|---|---|
| TextView | 显示一段只读文字 | android:text="你要显示的文字" |
| Button | 可点击的按钮 | android:text="按钮上的文字" |
| ImageView | 显示一张图片 | android:src="@drawable/molecule" |
| EditText | 输入和编辑文字的输入框 | android:hint="提示文字" |
| CheckBox | 多选框(可选多个) | android:text="复选框旁边的文字" |
| RadioGroup + RadioButton | 单选框(只能选一个,需配合使用) | android:orientation="vertical"(RadioGroup) |
除了上述关键属性,组件之间还有一些通用属性,先前已有过涉猎:
android:id="@+id/button":给控件一个唯一 id,好让代码能通过findViewById()找到它。android:layout_width和android:layout_height指定控件的宽度和高度。
一个垂直单选按钮示例
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"
android:padding="16dp"
android:background="#f5f5f5">
<!-- 标题 -->
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="请选择您喜欢的编程语言:"
android:textSize="18sp"
android:textStyle="bold"
android:textColor="#333333"
android:layout_marginBottom="16dp" />
<!-- 单选框组 -->
<RadioGroup
android:id="@+id/languageRadioGroup"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="@drawable/radio_group_background"
android:padding="16dp">
<!-- 第一个单选框 -->
<RadioButton
android:id="@+id/radioJava"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Java"
android:textSize="16sp"
android:textColor="#333333"
android:buttonTint="@color/radio_button_color"
android:padding="12dp" />
<!-- 第二个单选框 -->
<RadioButton
android:id="@+id/radioKotlin"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Kotlin"
android:textSize="16sp"
android:textColor="#333333"
android:buttonTint="@color/radio_button_color"
android:padding="12dp" />
<!-- 第三个单选框 -->
<RadioButton
android:id="@+id/radioPython"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Python"
android:textSize="16sp"
android:textColor="#333333"
android:buttonTint="@color/radio_button_color"
android:padding="12dp" />
</LinearLayout>| Layout | 作用/特点 | 关键属性 |
|---|---|---|
| LinearLayout | 子视图排成一条直线(垂直或水平) | android:orientation="vertical"/"horizontal"& android:layout_weight |
| RelativeLayout | 子视图可相对父容器或其他控件定位 | android:layout_alignParentBottom="true"& android:layout_toRightOf="@id/label" |
| TableLayout | 类似 HTML 表格,控件排成行和列 | android:layout_span(跨列)& android:stretchColumns(拉伸列宽) |
| ScrollView | 内容超出屏幕时可滚动,只能有一个子布局 | 无特殊属性 |
此外还有 ConstraintLayout(约束布局),你可以为每个 View 设置相对于别的 View 的约束条件(比如限制一个 View 右侧紧贴另一个 View 左侧)。该布局非常强大,正逐步取代 RelativeLayout,现在是 Android Studio 的默认布局方式。
但课程 ppt 里对于这个布局实际致只是一笔带过,故也不再详述。
weight - 权重
weight (权重) 用来决定在父容器中按比例分配空间。
Advance Layouts
这部分主要讲的是如何高效地显示列表数据。
Adapter 速览
ppt 里这部分没有介绍怎么创建自己的 Adapter。
ListView (或者 RecyclerView) 本身只是一个空的列表容器——它不知道要显示什么数据,也不知道每一行长什么样。你需要一个“数据管家”,这个管家就是 Adapter 。
Adapter 的工作是:
持有你的数据(ex:一个
List<String>)。当
ListView需要显示第5行时,Adapter 就会获取第5个数据。创建一个布局,把数据填充进去。
将填充好的 View 返回给
ListView去显示。
SimpleAdapter (简单适配器) 专门为 ListView 设计 ,专门用来把 List<Map<String, ?>> 里的数据显示出来。
构造函数
java
var adapter = new SimpleAdapter(this,
data,
R.layout.row_item,
new String[]{"name", "age"},
new int[]{R.id.nameTextView, R.id.ageTextView});data:你的数据,一个 Map 列表。resource:你为单独一行设计的 XML 布局文件。from:一个字符串数组,告诉SimpleAdapter你要从 Map 里取哪些键的数据。to:一个整数数组,告诉SimpleAdapter你要把数据填充到哪个控件里。
RecyclerView
由于 ListView 性能不好,现在已经被 RecyclerView 完全取代。
它之所以性能好,是因为它会回收 (Recycle) 视图。当一行滚出屏幕时,它不会销毁那行的 View,而是会回收它——用新数据填充 View,然后显示在刚滚进屏幕的底部。
添加依赖
dependencies {
implementation 'com.android.support:recyclerview-v7:28.0.0'
}在 XML 中使用
在你的 Activity 布局里,像普通控件一样添加它。
xml
<android.support.v7.widget.RecyclerView
android:id="@+id/my_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent" />在代码中使用
在 onCreate() 里,你必须为 RecyclerView 设置两个关键组件 :
LayoutManager: 这是 RecyclerView 比 ListView 强大的地方。LayoutManager 决定列表是如何排列的。new LinearLayoutManager(this):垂直列表。new LinearLayoutManager(this, LinearLayoutManager.VERTICAL, false):水平排列。new GridLayoutManager(this, 2):两列的网格布局。
Adapter(适配器): 需要一个 Adapter 来管理数据和创建视图。
示例
java
public class MainActivity extends Activity {
private RecyclerView recyclerView;
private RecyclerView.LayoutManager layoutManager;
private RecyclerView.Adapter mAdapter;
private List<String> myDataset;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 找到布局中的 RecyclerView 控件
recyclerView = (RecyclerView) findViewById(R.id.my_recycler_view);
// 初始化数据
myDataset = new ArrayList<>(Arrays.asList("第一行数据", "第二行数据", "第三行数据", "这是第四行", "Hello World"));
// 关联 LayoutManager
layoutManager = new LinearLayoutManager(this);
recyclerView.setLayoutManager(layoutManager);
// 创建 Adapter 并与 RecyclerView 关联
mAdapter = new MyAdapter(myDataset);
recyclerView.setAdapter(mAdapter);
}
}CardView
CardView 是一个容器,能把你放进去的任何布局(比如一个 LinearLayout)变成一张漂亮的卡片,自动带上圆角和阴影。
它非常常用,通常被用作 RecyclerView 列表里的一行内容的根布局,让你的列表看起来更现代。
添加依赖
dependencies {
implementation 'androidx.cardview:cardview:1.0.0'
}使用
你像使用普通布局一样使用它,它会自动给里面的 View 加上卡片背景。例如:
xml
<android.support.v7.widget.CardView
android:id="@+id/card_view"
android:layout_width="200dp"
android:layout_height="200dp"
card_view:cardCornerRadius="4dp">
<TextView
android:id="@+id/info_text"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</android.support.v7.widget.CardView>Action Bar
Action Bar(或 ToolBar)就是显示在应用屏幕最顶部的那条栏
Action Bar 是通过主题 (Theme) 来控制的。这意味着当你在 AndroidManifest.xml 里选择一个带 Action Bar 的主题(ex: Theme.Holo),它就会显示出来,反之亦然(ex: Theme.Holo.NoActionBar)。
xml
<application
android:theme="@style/Theme.AppCompat.Light.DarkActionBar">
<!-- ... -->
</application>当然你也可以选择一个有 Action Bar 的主题,然后在代码里动态显隐:
java
// 获取 Action Bar
ActionBar actionBar = getActionBar();
// 显隐
actionBar.hide();
actionBar.show();想要为 Action Bar 添加自定义的按钮是很常见的需求,也需要先定义 XML 布局然后在代码中加载。
在 XML 中定义
- 你需要在
res/文件夹下创建一个新的menu文件夹。 - 在
res/menu/文件夹里,创建一个 XML 文件(比如main_activity_menu.xml)。 - 在这个 XML 文件里,使用
<menu>作为根标签,里面用<item>标签来定义每一个按钮。
例如:
xml
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_save"
android:icon="@drawable/ic_menu_save"
android:title="@string/menu_save"
android:showAsAction="ifRoom" />
<item
android:id="@+id/menu_search"
android:icon="@drawable/ic_menu_search"
android:title="@string/menu_search"
android:showAsAction="ifRoom|collapseActionView"
android:actionViewClass="android.widget.SearchView" />
<item
android:id="@+id/action_settings"
android:title="@string/action_settings"
android:showAsAction="never" />
</menu>android:title:按钮文字。(@string/menu_save,指向strings.xml里的内容。)android:icon:按钮图标。android:showAsAction:按钮显示方式。取值 含义 ifRoom有空间时显示为图标,否则折叠进“...”(Overflow)菜单 withText同时显示图标和文字 always永远显示为图标,可能挤占空间 never永远不显示为图标,只在“...”菜单里显示文字
代码加载
接下来在 Activity 中加载菜单,你要在需要 Action Bar 的 Activity 中重写 onCreateOptionsMenu() 这个回调。
java
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// 获取一个菜单加载器
MenuInflater inflater = getMenuInflater();
// 使用加载器,把 main_activity_menu.xml 加载到 menu 对象里
inflater.inflate(R.menu.main_activity_menu, menu);
return true;
}响应 Action Bar
想要接收来自 Action Bar 上的事件,你需要在 Activity 中重写 onOptionsItemSelected() 回调 。每当用户点击任何一个 Action Bar 按钮时,系统都会调用这个方法,并把被点击的那个 MenuItem (菜单项) 传给你。
例如:
java
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// 通过 item.getItemId() 来判断用户到底点了哪个按钮
switch (item.getItemId()) {
case R.id.menu_save:
// 用户点了“保存”按钮
// ..
return true; // 表示事件已被处理
case R.id.menu_settings:
// 用户点了“设置”按钮
// ..
return true;
case android.R.id.home:
// 用户点了左上角的“向上”箭头
// 执行返回主页的操作
Intent intent = new Intent(this, HomeActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
return true;
default:
// 如果不是我们认识的 ID,就交给父类去处理
return super.onOptionsItemSelected(item);
}
}Intent
Intent (意图) 本质上是一个消息传递机制,用于“声明你的意图”,你可以用它来做三件大事:
- 启动一个 Activity
- 启动一个 Service
- 广播 (Broadcast) 一个事件
Explicit Intents (显式意图)
你明确指定要启动哪个组件。主要用于在你自己应用的内部进行跳转。因为你知道你自己所有 Activity 的类名。
java
// 明确告诉 Intent:我要启动 MyOtherActivity.class
Intent intent = new Intent(MyActivity.this, MyOtherActivity.class);
// 启动它
startActivity(intent);Implicit Intents(隐式意图)
不指定具体的组件,只声明一个通用的动作 (Action)。主要用于在应用之间进行通信。
例如:你不需要知道相机 App 的类名叫什么,你只需要发出拍照的动作,系统就会帮你找到所有安装了的、能拍照的 App 供用户选择。
java
// 只声明一个 Action:"android.media.action.IMAGE_CAPTURE" (拍照)
Intent intent = new Intent("android.media.action.IMAGE_CAPTURE");
startActivityForResult(intent, 0);在隐式意图主要靠参数 Action + Category + MIME Type 来描述一个任务。
| 参数 | 说明 | 示例 |
|---|---|---|
| Action | 要做什么 | ACTION_VIEW (查看)、ACTION_CALL (打电话)、ACTION_EDIT (编辑)、ACTION_SEND (发送) |
| Category | 动作的附加信息(非必须) | LAUNCHER & DEFAULT |
| MIME Type | 数据的类型(非必须) | text/plain (纯文本)、image/* (任意图片格式)、text/html (HTML网页)、audio/* (任意音频格式)、video/* (任意视频格式) |
| Component | 指定了要接收这个 Intent 的具体类名(变为显示) | com.example.app.MainActivity |
如果你觉得 Category 的介绍很模糊,是因为 ppt 就是这样模糊地写的。
启动 Activity
当你启动一个 Activity 时,有两种方式:
startActivity(intent)直接启动一个新的 Activity,不关心 Activity 什么时候关闭,也不需要它给你任何反馈。
startActivityForResult(intent, requestCode)启动一个新的 Activity,并期待它在完成后返回一些结果给你。
requestCode是你自己定义的数字,用来标记本次请求。当被启动的子 Activity 结束时,系统会调用你原 Activity 的
onActivityResult()方法,你可以在这里处理返回的数据。java@Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode == YOUR_REQUEST_CODE) { if (resultCode == RESULT_OK) { // 获取返回的数据 String resultData = data.getStringExtra("key_name"); // ... } } }requestCode:你启动 Activity 时传入的请求码。resultCode:子 Activity 返回的结果码,通常是RESULT_OK或RESULT_CANCELED。data:一个 Intent,包含返回的数据。
Intent Routing
意图路由就是 Android 系统寻找合适组件来处理一个隐式意图的决策过程。
一个 Activity(或 Service)必须满足所有条件才能匹配成功:
- 支持 Intent 中指定的 Action(ex:
ACTION_VIEW)。 - 支持 Intent 中指定的数据类型 (MIME Type)(如果 Intent 里有的话,比如
text/plain)。 - 支持 Intent 中包含的所有类别 (Category)。
如果只找到一个匹配的组件,系统就会直接启动它;如果找到多个,系统会弹出一个选择器让用户选一个。如果一个都没找到,app 会奔溃,系统会抛出 ActivityNotFoundException 异常。
Intent Filters
app 通过 Intent Filter (意图过滤器) 告诉系统它能处理哪些隐式意图。
Intent Filter 是在 AndroidManifest.xml 里定义的。例如:
xml
<activity android:name=".OurViewActivity">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
</activity>当你安装了这个 App,系统就会记住:OurViewActivity 这个房间,能处理查看纯文本的请求。
TIP
<activity android:name=".OurViewActivity"> 标明了程序的入口是 OurViewActivity。
Broadcasts (广播)
Broadcast 是 Intent 的一种特殊用法。能收到的广播例如低电量,网络变化,短信到达等系统事件。即使你的 App 当时根本没有在运行,系统也会启动它来接收广播。
App 也可以发送自定义广播,但 ppt 暂无涉及。
通过 BroadcastReceiver 来接收广播。你需要创建一个继承自 BroadcastReceiver 的类,并实现 onReceive() 方法。
java
public class MyBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// 处理接收到的广播
String action = intent.getAction();
if (action.equals(Intent.ACTION_BATTERY_LOW)) {
// 处理低电量事件
}
}
}你需要在 AndroidManifest.xml 中注册你的 BroadcastReceiver,并指定它要监听的广播类型(静态注册)。
xml
<!-- 注册 BroadcastReceiver -->
<receiver android:name=".MyBroadcastReceiver">
<intent-filter>
<!-- 监听低电量事件 -->
<action android:name="android.intent.action.BATTERY_LOW" />
</intent-filter>
</receiver>WARNING
如果你不在 AndroidManifest.xml 里写,而是在 Activity 或 Service 的代码中调用 registerReceiver() 来注册的话,那就是动态注册。你需要在 Activity 被销毁时调用 unregisterReceiver() 来注销它,否则会引发内存泄漏。
Resource Qualifiers
本章尝试解决一个核心问题:我的App如何才能在不同语言、不同屏幕尺寸的手机都良好运行?
Resource Qualifiers (资源限定符) 可以作为一个解决方案。它们是一些附加在资源文件夹(ex: res/values-zh/)名后的特殊标记,用来告诉系统:这些资源适用于特定的设备配置 (Configuration)。
文件夹格式:res/<资源名>-<限定符>
语言适配
假设你的 App 需要支持英文、爱尔兰盖尔语和简体中文:
res/values/strings.xml(默认 - 必须有) 存放默认语言,比如英文res/values-ga/strings.xml(爱尔兰盖尔语)res/values-zh/strings.xml(简体中文)
当一个系统语言为中文的手机来运行你的 App 时,系统会自动去 values-zh/ 文件夹里找字符串。
屏幕适配
res/layout/activity_main.xml(默认) 手机上用的单栏布局res/layout-large-land/activity_main.xml(限定符:大屏幕 + 横屏) 平板电脑横屏时用的双栏布局
常用类型
- 语言/地区:
en(英语),ga(盖尔语),en-rIE(爱尔兰地区的英语) - 屏幕尺寸:
small,normal,large,xlarge - 最小宽度 (Smallest Width):
sw<N>dp(比如sw600dp)。这是现代 Android 开发推荐的方式,sw600dp意味着“最小宽度大于600dp的设备”(通常指平板) - 屏幕朝向:
port(竖屏),land(横屏) - 屏幕密度 (DPI):
ldpi,mdpi,hdpi,xhdpi... (用来放不同分辨率的图片)
App Widget
App Widgets (应用小组件) 是个有趣的小功能:它是微型的应用视图,可以被嵌入到其他应用中,比如手机主屏幕或锁屏。
但其本身的限制很多:
- 限制性 Activity:功能有限的 Activity
- 性能和安全: 出于性能和安全考虑,它们受到的限制非常多
- 交互有限: 用户只能通过触摸 (Touch) 或垂直滑动 (Vertical swipe) 来与它交互
创建 App Widget
步骤:
- 设计 UI 布局 (Layout):创建一个XML 布局 (ex:
widget_layout.xml)。 - 添加元数据 (Metadata):创建一个
xml/文件夹并添加一个..._info.xml文件来描述你的小组件。 - 实现 Intent Receiver (接收者):创建一个
AppWidgetProvider类来处理小组件的更新逻辑。
我们来慢慢走一下这三步,首先在 res/layout/ 下创建一个布局文件 widget_layout.xml。
xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white" >
<TextView
android:id="@+id/widget_text"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:text="呜哇呜哇呜哇我是小组件"
android:gravity="center"
android:textSize="18sp" />
</FrameLayout>注意在此布局中,你不能使用任意的 View 和 Layout 和复杂控件 (ex: EditText, CheckBox, VideoView)。
可用的布局:
FrameLayoutLinearLayoutRelativeLayoutGridLayout
可用的控件:
- 主要是简单控件:
Button,ImageView,TextView,ProgressBar等 - 集合视图,如
ListView或GridView
第二步,在 res/xml/ 下创建一个 my_widget_info.xml 文件。这个文件用来告诉系统你的小组件的各种属性,比如初始布局、最小尺寸、更新频率等。
xml
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android">
<!-- 关联初始布局 -->
android:initialLayout="@layout/widget_layout"
android:minWidth="146dp"
android:minHeight="146dp"
android:label="My App Widget"
<!-- 更新频率 -->
android:updatePeriodMillis="3600000"
</appwidget-provider>最后实现 AppWidgetProvider,它其实是一个特殊的 BroadcastReceiver。它负责接收来自系统的小组件广播,并处理小组件的更新和交互逻辑。
java
public class MyAppWidgetProvider extends AppWidgetProvider {
// 当小组件需要更新界面时调用——最重要的方法
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
// 更新小组件 UI ——ppt 里还是没介绍这部分
// ...
}
// 第一个小组件被创建时调用
@Override
public void onEnabled(Context context) {
// ...
}
// 最后一个小组件被删除时调用
@Override
public void onDisabled(Context context) {
// ...
}
// 当小组件被删除时调用
@Override
public void onDeleted(Context context, int[] appWidgetIds) {
// ...
}
}接下来在 AndroidManifest.xml 里注册 MyAppWidgetProvider,让系统知道它的存在,并且关联 MyAppWidgetProvider 和元数据 my_widget_info.xml。
xml
<receiver android:name=".MyAppWidgetProvider" >
<!-- 处理小组件更新的 Intent -->
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/my_widget_info" />
</receiver>Notifications
它是一种在你的 App 界面之外提醒用户的机制(就是你手机顶部状态栏里弹出的那些图标和消息)。
创建和发送一个通知
获取
NotificationManagerNotificationManager是一个 Android 系统服务,你必须通过getSystemService()来获取它:javaNotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);使用 Builder (构建器) 来构造一个通知
NotificationCompat.Builder是一个好东西,可以向后兼容:在旧版本安卓上也能正常工作,在新版安卓上也能利用新特性。java// ppt 中使用的是 Notification.Builder NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(this) .setSmallIcon(R.drawable.notification_icon) // 设置小图标 (必须) .setContentTitle("My notification") // 设置标题 .setContentText("Hello World!"); // 设置正文定义一个 Notification ID
熟悉的,你又需要自定义一个整数 ID,用来标识这个通知,以便后续更新或取消它。如果你用相同的 ID 发送另一个通知,新的通知会覆盖旧的,可实现更新效果。
javaint notificationId = 001; mNotifyMgr.notify(notificationId, mBuilder.build());你也可以调用
mNotifyMgr.cancel(notificationId)来手动移除这个通知。发送通知
最后,你告诉
mNotifyMgr,用这个 ID 发送这个通知:javamNotifyMgr.notify(notificationId, mBuilder.build());
安卓8.0 (API 级别 26) 引入了通知渠道 (Notification Channels) 的概念,使开发者可以更好地管理和分类通知,你必须将一个通知分配到一个渠道中才能显示它。
ppt 也只是提了一句。
Toasts (吐司)
Toast 是一种更轻量级的提示,是一个小小的弹窗,更适合用在前台,显示简单的反馈信息。
- 不打断当前 Activity:面积小,当前 Activity 仍然可见且可交互。
- 自动消失:显示一段时间后会自动消失,无需用户操作。
Toast 的使用非常简单,你不需要 Manager 或 Builder,只需要一行链式调用:
java
// 1. 获取上下文 (Context)
Context context = getApplicationContext();
// 2. 要显示的文字
CharSequence text = "!!! 你的手机已被 LimBoo 病毒侵入 !!!!";
// 3. 显示的时长 (LENGTH_SHORT 或 LENGTH_LONG)
int duration = Toast.LENGTH_SHORT;
// 4. 创建并显示 Toast
Toast toast = Toast.makeText(context, text, duration);
toast.show();
// (更简洁的写法)
// Toast.makeText(this, "Hello toast!", Toast.LENGTH_SHORT).show();安卓6.0前——安全性
Sandboxing (沙盒机制)
沙盒机制是安卓在安全和性能开销权衡后设计的一种机制,是通过 Linux 的内核安全机制实现的。
- 进程独立:每个 app 运行在自己独立的进程当中。
- 独立ID (Unique UID): 系统会为每一个 App 分配一个唯一的用户ID (UID) ,不同 app 视作不同用户。
- 独立虚拟机 (Own VM):每个 App 运行在它自己的 Dalvik/ART 虚拟机实例中。
沙盒实现了进程间的隔离(isolated)和安全(secure),一个 app 的崩溃不会影响到其他 app。
Digital Signatures (数字签名)
每个 APK 都必须经过开发者签名,这保证了应用的来源和完整性。
获取 Permissions (权限)
沙盒把 App 关在了一个密室里。App 若要与外界通信,需要通过 Permissions (权限)。App 必须明确地请求它需要的每一项权限。
在 AndroidManifest.xml 文件中声明权限:
xml
<uses-permission
android:name="android.permission.RECEIVE_SMS" /> <!-- 接收短信权限 -->
<uses-permission
android:name="android.permission.INTERNET" /> <!-- 访问网络权限 -->
<uses-permission
android:name="android.permission.ACCESS_LOCATION" /> <!-- 访问位置信息权限 -->权限是静态的(static),这意味着在安装 App 时,系统会向用户显示一个列表,列出这个 App 请求的所有权限。用户只有两个选择:“全部同意并安装” 或 “全部拒绝并不安装”。一旦安装完成,权限就不能被修改,除非用户卸载它。
创建自己的 Permissions
你还可以创建自己的权限,用来保护你自己的 App 组件,比如你的 Activity 或 Service。
同样在 AndroidManifest.xml 里定义:
xml
<permission
android:name="com.me.app.myapp.permission.DEADLY_ACTIVITY"
android:label="@string/permlab_deadlyActivity"
android:description="@string/permdesc_deadlyActivity"
android:permissionGroup="android.permissiongroup.COST_MONEY"
android:protectionLevel="dangerous" />android:name 是权限的唯一名称,通常以包名开头以避免冲突。此处最值得一提的属性是 android:protectionLevel,它定义了权限的保护级别:
normal:低风险,系统会自动授予,不需要用户同意。dangerous:高风险,用户必须明确同意才能授予。signature:非常重要。系统只会把这个权限授予那些和你这个 App 使用了相同签名证书的 App。这是保护你自己公司内部 App 之间通信的最佳方式。signatureOrSystem:类似于signature,但也允许系统 App 使用这个权限。
在你的 Activity 或 Service 里使用这个权限:
xml
<activity
android:name=".DeadlyActivity"
android:permission="com.me.app.myapp.permission.DEADLY_ACTIVITY" >
<!-- ... -->
</activity>TIP
- 尽量使用 HTTPS,而不是 HTTP
- 如果你的 Activity、Service 或 BroadcastReceiver 只是给你自己 App 内部用的,不要导出它们(即在
AndroidManifest.xml中设置android:exported="false"),防止被其他 App 调用 。 - 最小权限原则 (Principle of Least Privilege):不要申请你用不到的敏感权限。
安卓6.0及以后——安全与权限
从安卓6.0 (API 级别 23) 开始,引入了运行时权限 (Runtime Permissions) 模型,改变了用户授权权限的方式。
权限分为两类:
Normal Permissions (普通权限)
低风险权限,它们不会访问用户的敏感隐私数据,如网络服务。
和老模型一样的声明方式。
Dangerous Permissions (危险权限)
高风险权限,因为它们会访问用户的敏感数据或资源,比如隐私、硬件。
你必须先在
AndroidManifest.xml里声明它 。并且,你还必须在 App 运行时(比如用户点击“拍照”按钮时),用代码动态地弹出一个对话框来请求用户同意。用户可以单独授予或拒绝这个权限。
本节聚焦于请求 Dangerous Permissions。
首先,和以前一样,在 AndroidManifest.xml 里声明权限,然后在代码里去检查这个权限,如果没有被授予,就请求它。
例如,我们检查按下拍照按钮时,App 是否有使用相机的权限:
java
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
// 如果不等于"已授予",说明没有权限
} else {
// 我已经有权限了,直接打开相机
}如果没有权限,我们就请求它:
java
// 弹出一个系统对话框,向用户请求 CAMERA 权限
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.CAMERA},
MY_CAMERA_REQUEST_CODE);
// (MY_CAMERA_REQUEST_CODE 是你自己定义的整数,比如 101)当用户在弹窗上点击“允许”或“拒绝”后,系统会自动调用你 Activity 里的 onRequestPermissionsResult 回调:
java
@Override
public void onRequestPermissionsResult(int requestCode,
String[] permissions, int[] grantResults) {
// 检查是不是“相机权限”的请求回来了
if (requestCode == MY_CAMERA_REQUEST_CODE) {
// 检查“授权结果”数组
if (grantResults.length > 0 &&
grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// 用户点击了“允许”
// 在这里打开相机
} else {
// 用户点击了“拒绝”
// 你必须“优雅地处理拒绝” (Handle Denials Gracefully)
// 比如:显示一个 Toast,告诉用户“没有相机权限,无法拍照”
}
}
}Data Storage
主要有四种方式来 CRUD 数据:
- Preferences (偏好设置)
- Plain files (普通文件)
- Local database (本地数据库)
- Network (网络)
Preferences
一个用来存储少量、原始类型数据(比如 String, int, boolean)的机制。比如存用户偏好设置,夜间模式是否开启(true/false)、欢迎页面的用户名 (String) 之类的。Android 系统会把你的这些设置保存为一个 XML 文件。
有两种方式来使用 Preferences:
getPreferences():(Activity 专用) 只想给当前 Activity 存一个私有的配置文件。getSharedPreferences(String name, int mode):(App 级别)你给它一个名字(比如"myPrefs"),就可以在 App 的任何地方访问这个共享的配置文件——推荐。
你需要通过一个 Editor 来修改 Preferences:
java
// 1. 获取 SharedPreferences 对象
SharedPreferences settings = getSharedPreferences("myPrefs", 0);
// 2. 获取 Editor 对象
SharedPreferences.Editor editor = settings.edit();
// 3. 放入数据 (键值对)
editor.putString("username", "Abey");
// 4. 提交更改
editor.commit(); // 或者用 editor.apply(),它在后台异步提交DataStore 是 Google 新推出的、用来替代 SharedPreferences 的方案,更安全、更高效。
Plain Files
其实就是标准的 Java 文件 I/O (输入/输出)。当 SharedPreferences 不够用时,比如你要保存一个 JSON 文件、一个日志文件,或者一张用户下载的图片,考虑使用普通文件存储。
java
String FILENAME = "hello_file";
String string = "hello world!";
// 1. 打开一个私有的文件输出流 (FileOutputStream)
// Context.MODE_PRIVATE 意味着这个文件只能被你自己的 App 访问
FileOutputStream fos = openFileOutput(FILENAME, Context.MODE_PRIVATE);
// 2. 写入数据
fos.write(string.getBytes());
// 3. 关闭
fos.close();SQLite & Room
这是最强大、最复杂的数据存储方式。当你需要存储大量、复杂的、结构化数据时,考虑使用本地数据库,比如:一个日记 App 的所有日记、一个音乐 App 的所有歌曲信息、一个购物 App 的订单列表。
Android 内置了 SQLite 数据库引擎,你可以直接使用 SQLite API 来创建和管理数据库。
在以前的开发中,你需要创建一个继承自 SQLiteOpenHelper 的类,重写 onCreate() 和 onUpgrade() 方法,手动编写 SQL 语句来创建和更新数据库表。这非常繁琐、容易出错,需要写大量“模板代码”。
Room 是 Google 推荐的方案。它是在 SQLite 之上加了一层抽象层。通过 DAO (数据访问对象) 和注解 (Annotations),你可以用更少的代码来操作数据库,类似 MyBatis。
java
@Dao
public interface UserDao {
// 插入数据
@Insert
void insertUser(User user);
// 查询数据
@Query("SELECT * FROM users WHERE uid = :userId")
User getUserById(int userId);
}怎么用这个接口?ppt 里没说!
Room 不允许你在主线程(UI线程)上执行数据库查询,避免你的 App 因为数据库操作而卡顿。
Network
数据也可以通过网络存储在云端服务器上。
云服务供应商(CSP):
- Firebase (Google)
- AWS Mobile (Amazon)
- Azure Mobile (Microsoft)
- Alibaba cloud services (阿里云)