ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Android入门实验报告:环境搭建、Activity跳转与UI布局实战

Android入门实验报告:环境搭建、Activity跳转与UI布局实战

简介:面向软件学院《移动应用软件开发技术》课程的实验报告文档,围绕 Android 平台开发的基础实践展开,适合刚接触移动应用开发、需按实验流程完成环境配置与入门练习的本科生对照使用。内容按实验项目组织:实验一梳理 Android 系统环境搭建,涵盖 JDK 6.0 以上版本的安装与 JAVA_HOME 环境变量配置、Eclipse IDE for Java EE Developers 的解压与启动验证、Android SDK 在线更新与离线安装两种方式及 ADT 插件安装;后续实验延伸到 Activity 的创建、启动、生命周期管理与 Intent 跳转,并分两部分讲解 TextView、Button、ImageView 等基础控件与 LinearLayout、RelativeLayout、GridLayout 等布局管理器的用法。压缩包含 1 个 doc 文件,约 1.63MB,保留实验目的、内容原理、截图占位与结果记录要求,便于对照实验思路与报告排版。已有 768 人学习。

1. 从一份实验报告说起:三个实验串起 Android 入门的完整链路

移动应用软件开发技术这门课的实验报告,常见的交法是四个实验各截几张图,拼成一份文档:装环境、点按钮跳页面、拖几个控件、弹一个对话框。交上去能过,但答辩时被问一句“从 A 页面跳到 B 页面再返回,生命周期走了哪几步”,很多人就答不上来。问题不在截图数量,而在于这份报告本来应该把 Android 入门的三条主线——环境链路、组件跳转、界面组织——按可验证的顺序串起来,结果被拆成了互不相干的练习。下面按实验真实顺序展开,把每一步的安装顺序、源码结构、布局拆解和验收点都落到能直接对照的细节上,适合要交课程设计、实验报告或准备入门答辩的人拿去改。

2. 开发环境搭建:JDK、Eclipse、SDK、ADT 四件套的安装顺序与验证点

环境搭建这一步看起来最没技术含量,实际上是最容易卡住半天的地方。实验一要求装 JDK 6.0 以上、Eclipse、Android SDK 和 ADT 四样东西,很多人拿到清单就开始装,装到一半 Eclipse 双击没反应,回头才发现 JDK 没配好。这一章把四个组件的依赖关系、每条命令的含义和每一步的验证点讲清楚,照着走基本不会返工。

2.1 为什么安装顺序不能乱

这四个组件是一条单向依赖链:JDK 提供 javac 和 java 两个命令;Eclipse 本身是 Java 程序,启动时先找 JRE,找不到就直接退出;Android SDK 里的 SDK Manager 是一个 Java 写的图形工具,同样依赖 JDK;ADT 是 Eclipse 插件,必须装在一个能正常启动的 Eclipse 上;ADT 装完之后还要手动把 SDK 的根目录指给它,整条链才算闭合。

所以正确顺序是 JDK → 配置环境变量 → Eclipse → SDK → ADT → 绑定 SDK 路径。反过来先装 Eclipse 再装 JDK 的,启动时经常弹一个 “Failed to load the JNI shared library”,本质是 Eclipse 按自己找到的 JRE 版本去加载 64 位或 32 位的 jvm.dll,位数对不上。实验环境里的 Windows 7 笔记本是 32 位系统,配 32 位 JDK 和 32 位 Eclipse,这条链才顺。

我一般会在装之前先确认三件事:系统位数、JDK 安装路径里有没有空格或中文、Eclipse 压缩包解压到哪个盘。路径带中文在 Windows 上不一定立刻出问题,但 SDK Manager 下载组件时偶发写入失败,排查起来就是黑匣子,不如一开始就避开。

2.2 环境变量配置与 test.java 冒烟测试

JDK 装完只是把文件放到硬盘上,命令行还不认识 java 命令,必须配环境变量。右击“我的电脑”→“属性”→“高级”→“环境变量”,在系统变量里新建JAVA_HOME,值填 JDK 的安装根目录,注意不是 bin 目录。然后在Path末尾追加%JAVA_HOME%\bin,这样命令行在任何目录下都能找到 java 和 javac。

老教材还会要求配CLASSPATH,值是.;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jar。JDK 6 之后其实不配也能跑,但配了不影响,重点是开头那个英文句点,它代表当前目录,漏掉的话运行自己编译出来的 class 会报 NoClassDefFoundError,这个坑当年坑过不少人。

配完要验证,实验要求写一个 test.java 跑通。这段代码的作用不只是打印一句话,更重要的是反查 JVM 到底用了哪个 JDK:

public class test { public static void main(String[] args) { System.out.println("JDK 环境就绪"); // 打印 java.home,确认当前 JVM 来自哪个 JDK 目录 System.out.println("java.home = " + System.getProperty("java.home")); // 打印版本号,确认不是系统自带的老 JRE System.out.println("java.version = " + System.getProperty("java.version")); } }

编译和运行命令如下。第一行把源码编译成 test.class,第二行启动 JVM 执行:

javac test.java java test

这里有个细节:类名必须和文件名一致,Windows 文件系统不区分大小写,写成 Test.java 也能编译,但拷到 Linux 环境就报错,养成首字母大写的习惯更稳。另外执行时写java test而不是java test.class,JVM 要的是类名不是文件名。输出里 java.home 指向的目录如果和 JAVA_HOME 一致,说明环境变量生效了;如果指向C:\Windows\System32下面某个 jre 目录,说明 Path 里系统自带的 Java 排在前面,需要把它往后调。

2.3 SDK 的在线与离线两条路

Android SDK 是整个环境里体积最大的一块,实验给了在线和离线两种装法。在线方式是启动 SDK Manager,在列表里勾选 SDK Platform、Tools、Google APIs、Samples、USB Driver 这几类,勾上 Accept License 再点 Install,工具会自己下载解压。这种方式省事,但对网络稳定性有要求,中途断了容易出现下载了一半的临时目录。

离线方式是先下载android-sdk_r16-windows.zip这个安装工具包并解压,然后手动建目录、手动把各版本的 zip 解压进对应位置。实验清单里从 android-1.5_r04 一直列到 android-2.2_r01,还带了几个 google_apis 包,这些包解压后的目录名不是版本号字符串,而是android-<API Level>,对应关系是 1.5 对应 android-3、1.6 对应 android-4、2.0 对应 android-5、2.0.1 对应 android-6、2.1 对应 android-7、2.2 对应 android-8。搞错这一层,SDK Manager 就认不出来。清单里还夹着一个跟 Android 无关的中间件包,解压了也不会被识别,直接跳过。

装完的目录结构大致是这样,每个子目录的职责要分清楚:

android-sdk-windows/ ├── tools/ # SDK Manager、emulator 等管理工具 ├── platform-tools/ # adb、fastboot,ADT 会自动寻找 ├── platforms/ │ └── android-8/ # 由 android-2.2_r01-windows.zip 解压而来 │ ├── android.jar # 编译期引用的框架类库 │ ├── data/ │ └── templates/ ├── add-ons/ │ └── google_apis-8/ # 对应 google_apis-8_r01.zip ├── docs/ # 离线文档 ├── samples/ # 官方示例工程 ├── usb_driver/ # Windows 下的真机驱动 └── market_licensing/ # 应用市场授权相关库

最容易翻车的一步是把 zip 直接解压到platforms目录下,结果多出一层同名目录,变成platforms/android-2.2_r01-windows/android-8,SDK Manager 扫描时只认platforms下第一层的 android-* 目录,于是列表里空空如也。正确做法是解压后把里面真正的 android-8 目录挪到 platforms 下。离线安装的好处是不依赖网络,坏处是这一步的目录层级全靠手工保证,装完要打开 SDK Manager 确认 Installed 列表里有对应的条目。

2.4 ADT 插件安装与 SDK 路径绑定

ADT 的装法是在 Eclipse 里走 Help → Install New Software,点 Add 弹出仓库对话框,Name 随便填个 ADT,Location 填官方更新站点地址,确定后 Eclipse 会去搜索可安装项。搜到之后勾选 Developer Tools 这一组,一路 Next、接受协议、Finish,装完提示重启,选 Restart Now 让插件生效。整个过程几分钟,中间如果勾选项是灰的或者列表为空,多半是 Eclipse 版本和 ADT 版本对不上,这个放到后面的排查章细说。

重启后还有最后一步:Window → Preferences → Android,在 SDK Location 里填 SDK 的根目录,点 OK。这一步不做,新建工程时会提示找不到目标平台。绑定成功的判断标准是新建 Android Project 时,Build Target 列表里能列出刚才装的那些 API 版本。

四个组件各自的作用和装错时的典型症状可以对照下表:

组件作用装错时的典型症状
JDK提供 javac、java,编译和运行 Java 代码命令行提示“不是内部或外部命令”
Eclipse集成开发环境,承载 ADT 插件双击闪退,报 JNI shared library 错误
Android SDK提供各版本 android.jar、adb、模拟器新建工程时 Build Target 列表为空
ADT把 SDK 能力接进 Eclipse菜单里没有 Android 相关选项

3. Activity 与 Intent:从页面跳转到跨页面传值的两套写法

实验二的重点不是“点一下按钮能跳页”,而是搞清楚 Activity 在系统里怎么被注册、跳转时生命周期怎么走、数据通过什么载体传过去。这三件事在实验报告里往往只留了源码截图,实际上它们才是答辩时最容易被追问的部分。下面按两个小实验分别拆开,代码保持能直接编译的状态,同时在关键行写明为什么这么写。

3.1 两个 Activity 的注册与生命周期回调

Activity 不是写完类就能用的,必须在 AndroidManifest.xml 里登记,否则 startActivity 时直接抛 ActivityNotFoundException。实验里有两个 Activity,一个有启动图标,一个只能被代码拉起,配置差别就在 intent-filter:

<application android:icon="@drawable/ic_launcher" android:label="TestActivity01"> <!-- 带 MAIN/LAUNCHER 的 Activity 会出现在桌面启动器里 --> <activity android:name=".MainActivity" android:label="主界面"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <!-- 第二个 Activity 只需要登记类名,不需要启动图标 --> <activity android:name=".OtherActivity" android:label="第二界面" /> </application>

android:name前面的点表示相对当前包名,写成完整包名也等价。漏写这个节点是最常见的崩溃来源,而且编译器不会报错,只有运行时才暴露。

生命周期的顺序在报告里往往一句话带过,实际跳转时的回调链条是这样的:A 的 onPause 先执行,然后 B 依次走 onCreate、onStart、onResume,等 B 完全可见之后 A 才走 onStop。返回时反过来,B 的 onPause、A 的 onRestart、A 的 onStart、A 的 onResume,最后 B 走 onStop 和 onDestroy。把这个顺序记住,答辩时问“为什么在 onPause 里保存数据”就能答上来。

回调方法触发时机通常在这里做什么
onCreateActivity 第一次创建setContentView 绑定布局、findViewById 拿控件
onStart界面即将可见注册广播、刷新列表数据
onResume界面可交互恢复动画、重新申请定位等
onPause失去焦点但还可见保存临时输入、暂停耗时操作
onStop完全不可见释放大对象、注销监听
onDestroy实例被销毁收尾清理,避免内存泄漏

3.2 TestActivity01:按钮触发的显式跳转

第一个小实验要求主界面只有一个按钮,点击后打开第二个界面,第二个界面只有一个 TextView。主界面的写法如下,重点是 findViewById 的调用时机和 Intent 的构造方式:

package com.example.test1; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.view.View; import android.widget.Button; public class MainActivity extends Activity { private Button jumpBtn; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // 绑定布局,必须先执行这一句,后面才能按 id 找控件 setContentView(R.layout.activity_main); jumpBtn = (Button) findViewById(R.id.button1); jumpBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // 显式 Intent:直接指定目标 Activity 的 Class 对象 Intent intent = new Intent(MainActivity.this, OtherActivity.class); startActivity(intent); } }); } }

参数说明几个关键点:R.layout.activity_main是编译器为res/layout/activity_main.xml生成的常量;R.id.button1对应布局里android:id="@+id/button1"的声明,@+id里的加号表示新建 id,在其他文件里引用时不加加号;MainActivity.this而不是this,因为写在匿名内部类里,直接写 this 指的是 OnClickListener,类型对不上,编译期就会报错。

第二个界面更简单,只负责把布局挂上去并给 TextView 赋值:

package com.example.test1; import android.app.Activity; import android.os.Bundle; import android.widget.TextView; public class OtherActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.two); TextView tip = (TextView) findViewById(R.id.ray); tip.setText("my first activity"); } }

两个布局文件都很短,用 RelativeLayout 定位即可:

<!-- res/layout/activity_main.xml --> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <Button android:id="@+id/button1" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentLeft="true" android:layout_alignParentTop="true" android:layout_marginLeft="106dp" android:layout_marginTop="165dp" android:text="my_activity" /> </RelativeLayout>
<!-- res/layout/two.xml --> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent"> <TextView android:id="@+id/ray" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentTop="true" android:text="my first activity" /> </RelativeLayout>

这里刻意没有保留工程模板生成的android:paddingLeft="@dimen/activity_horizontal_margin"这类属性,因为它们依赖res/values/dimens.xml里预先定义好的尺寸,模板工程换成空工程时这些引用会直接导致编译失败。报告里贴源码截图时,如果报错行指向 @dimen,基本就是这个原因。

3.3 TestActivity02:用 Bundle 把字符串带到下一个界面

第二个小实验在跳转的基础上加了一件事:把主界面里的“中国”两个字传到第二个界面显示。实现上有两种写法,实验用的是 Bundle 加 Intent:

// 主界面按钮点击事件内 Intent testIntent = new Intent(); Bundle testBundle = new Bundle(); // key 为 name,值为字符串“中国” testBundle.putString("name", "中国"); testIntent.putExtras(testBundle); // 指定目标 Activity testIntent.setClass(TextMainActivity.this, SecondActivity.class); startActivity(testIntent);

接收端从 Intent 里把 Bundle 取出来,再按同一个 key 拿值:

// 第二个界面的 onCreate 内 Bundle extra = getIntent().getExtras(); // getExtras() 可能返回 null,必须先判空再取值 if (extra != null) { String name = extra.getString("name"); TextView show = (TextView) findViewById(R.id.ruirui); show.setText(name); }

setClass和new Intent(this, Class)效果一样,前者适合 Intent 对象需要复用的场景,比如动态决定跳到哪个页面时先用空 Intent 再逐个 set。getExtras()的判空不能省,如果目标界面是由桌面图标直接启动的(没经过主界面),Intent 里根本没有 Bundle,直接getExtras().getString()会空指针崩溃。这是实验报告里最容易被扣分的一个细节,因为老师通常会手点两次来测。

3.4 传值方式的取舍

实验只演示了字符串这一种类型,实际工程里传的东西更多。基本类型和 String 可以直接intent.putExtra("key", value)一行写完,不必绕 Bundle;多个字段一起传时 Bundle 更整齐,key 集中在一处不容易拼错;传自定义对象则要求对象实现 Serializable 或 Parcelable,前者写起来简单但效率低,后者要手写一堆样板代码但跨进程也能用。

方式适用场景注意点
putExtra 直传单个基本类型或 Stringkey 两端必须完全一致
Bundle 打包多个字段一起传读取端要判空
Serializable传对象,量小效率一般,慎传大对象
Parcelable传对象,追求效率需要实现一堆方法
startActivityForResult需要从下一页拿回结果新版框架里已被更规范的写法替代

4. UI 设计实验:布局嵌套、控件组合与对话框交互

实验三分两部分,前半部分考布局嵌套,后半部分考对话框。布局这块的核心不是记住哪个属性叫什么,而是拿到一张效果图之后,能判断该用几层容器、每层用什么布局、权重怎么分配。这部分在报告里如果只贴 XML 不写拆解思路,看起来就是一堆属性堆砌,答辩时说不清为什么这么写。

4.1 三段式界面的拆解思路

实验给的效果图是一个纵向排布、分成三大块的界面:最上面是四列等宽的色块,中间是三行三列的表格,最下面是四行内容。拆解顺序是从外到内:最外层所有内容都是从上往下排,用纵向 LinearLayout;三段内部结构互不相同,各自单独用一层容器,横向 LinearLayout、TableLayout、纵向 LinearLayout 各管一段。

最外层的骨架长这样,三个子容器各占三分之一高度,靠 layout_weight 分配:

<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent" android:orientation="vertical"> <!-- 第一段:横向 LinearLayout,四列等宽 --> <LinearLayout android:layout_width="fill_parent" android:layout_height="0dp" android:layout_weight="1" android:orientation="horizontal"> <TextView android:layout_width="0dp" android:layout_height="fill_parent" android:layout_weight="1" android:background="#CE0000" android:gravity="center" android:text="red" /> <TextView android:layout_width="0dp" android:layout_height="fill_parent" android:layout_weight="1" android:background="#00A600" android:gravity="center" android:text="green" /> <TextView android:layout_width="0dp" android:layout_height="fill_parent" android:layout_weight="1" android:background="#0062CE" android:gravity="center" android:text="blue" /> <TextView android:layout_width="0dp" android:layout_height="fill_parent" android:layout_weight="1" android:background="#666666" android:gravity="center" android:text="gray" /> </LinearLayout> <!-- 第二段:TableLayout,三行三列 --> <TableLayout android:layout_width="fill_parent" android:layout_height="0dp" android:layout_weight="1" android:stretchColumns="*"> <TableRow> <TextView android:gravity="center" android:text="r1c1" /> <TextView android:gravity="center" android:text="r1c2" /> <TextView android:gravity="center" android:text="r1c3" /> </TableRow> <TableRow> <TextView android:gravity="center" android:text="r2c1" /> <TextView android:gravity="center" android:text="r2c2" /> <TextView android:gravity="center" android:text="r2c3" /> </TableRow> <TableRow> <TextView android:gravity="center" android:text="r3c1" /> <TextView android:gravity="center" android:text="r3c2" /> <TextView android:gravity="center" android:text="r3c3" /> </TableRow> </TableLayout> <!-- 第三段:纵向 LinearLayout,四行 --> <LinearLayout android:layout_width="fill_parent" android:layout_height="0dp" android:layout_weight="1" android:orientation="vertical"> <TextView android:layout_width="fill_parent" android:layout_height="0dp" android:layout_weight="1" android:gravity="center" android:text="row 1" /> <TextView android:layout_width="fill_parent" android:layout_height="0dp" android:layout_weight="1" android:gravity="center" android:text="row 2" /> <TextView android:layout_width="fill_parent" android:layout_height="0dp" android:layout_weight="1" android:gravity="center" android:text="row 3" /> <TextView android:layout_width="fill_parent" android:layout_height="0dp" android:layout_weight="1" android:gravity="center" android:text="row 4" /> </LinearLayout> </LinearLayout>

这段代码值得注意的地方不在控件数量,而在于每个需要等分的子 View 都把主方向的尺寸设成0dp,再靠 weight 拿空间。写成wrap_content的话,TextView 会先占掉文字本身需要的宽高,剩下的空间才按 weight 分,结果就是四个色块宽度不一致,这是布局实验里最典型的翻车现场。

4.2 layout_weight 的计算规则

layout_weight 的分配逻辑是:父容器先把固定尺寸的子 View 排完,剩下的空间按各子 View 的 weight 比例分配。所以 weight 生效的前提是主方向上还有剩余空间可分配。如果子 View 的宽或高写的是match_parent,它自己就把空间占满了,weight 比例完全不起作用。

举个具体的例子:横向 LinearLayout 宽 320dp,里面两个 TextView,第一个宽 0dp、weight 为 1,第二个宽 0dp、weight 为 2,那第一个占 320×1/3,第二个占 320×2/3。如果第一个改成wrap_content且文字占 40dp,则剩余 280dp 按 1:2 分,第一个实际拿 40+93,第二个拿 187。数字对不上预期时,先检查是不是有子 View 用了 wrap_content。

属性写在谁身上含义
android:orientation父容器决定子 View 纵向还是横向排列
android:layout_weight子 View参与父容器剩余空间的分配比例
android:layout_width / height子 View等分时常设 0dp,避免内容撑开
android:gravity控件自身控制内部文字的对齐方式
android:layout_gravity子 View控制自己在父容器里的对齐方式

gravity 和 layout_gravity 这两个名字只差一个前缀,作用完全不同,报告里如果写混了,界面效果就是文字居中变成控件居中,答辩被指出来很尴尬。

4.3 TableLayout 的几个开关

TableLayout 的列宽默认由内容决定,要达到等宽效果需要开 stretchColumns。android:stretchColumns="*"表示所有列都拉伸填满剩余宽度,也可以写成具体列号,比如"0,2"只拉伸第一列和第三列。对应的还有 shrinkColumns 负责在空间不够时收缩指定列,collapseColumns 负责把指定列隐藏,这三个属性经常配合使用,做表单类界面时比一层层嵌套 LinearLayout 清爽得多。

行用 TableRow 包裹,TableRow 里的子 View 默认宽度是 wrap_content,由 TableLayout 统一管理。如果把 TableRow 换成普通 LinearLayout,整张表就退化成纵向堆叠,列对齐立刻失效,这也是一个常见笔误。

4.4 多输入框对话框与三种输出通道

实验三的第二部分是做一个带两个输入框的对话框,点确定后要把输入内容输出。界面用一个单独的布局文件描述,两个 TextView 做标签,两个 EditText 收输入:

<!-- res/layout/dialog_input.xml --> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="wrap_content" android:orientation="vertical" android:padding="16dp"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="姓名" /> <EditText android:id="@+id/et_name" android:layout_width="fill_parent" android:layout_height="wrap_content" android:inputType="text" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="年龄" /> <EditText android:id="@+id/et_age" android:layout_width="fill_parent" android:layout_height="wrap_content" android:inputType="number" /> </LinearLayout>

Java 侧把布局 inflate 成 View,塞进 AlertDialog,再在确定按钮里取值。这段代码有两个容易写错的地方,一是 findViewById 的宿主,二是内部类里引用外部 Activity:

// 把 dialog_input.xml 渲染成 View 对象 LayoutInflater inflater = LayoutInflater.from(this); View dialogView = inflater.inflate(R.layout.dialog_input, null); final EditText nameEt = (EditText) dialogView.findViewById(R.id.et_name); final EditText ageEt = (EditText) dialogView.findViewById(R.id.et_age); new AlertDialog.Builder(this) .setTitle("请输入信息") .setView(dialogView) .setPositiveButton("确定", new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { String name = nameEt.getText().toString().trim(); String age = ageEt.getText().toString().trim(); // 输出通道一:Toast 短暂提示 Toast.makeText(MainActivity.this, name + " / " + age, Toast.LENGTH_SHORT).show(); // 输出通道二:Logcat,按 tag DIALOG 过滤 Log.i("DIALOG", "name=" + name + ", age=" + age); } }) .setNegativeButton("取消", null) .show();

关键点是dialogView.findViewById(...),如果用 Activity 的 findViewById 去查对话框里的控件,返回的一定是 null,因为那些控件不在 Activity 的布局树里。这是自定义对话框最常见的空指针来源,现象是点确定就崩,日志里指向 getText() 那一行。

三个输出通道各有用途:Toast 适合给用户看,Logcat 适合自己调试,写回 Activity 里的 TextView 适合做持久展示。实验要求三种任选,报告里写两种以上会更完整,因为老师通常会问“如果用户手快连点两次会怎样”,这时候 Logcat 里能看到两条记录,Toast 会排队显示。

5. 实验报告踩坑排查:环境、跳转、布局三类问题的定位方法

实验过程中遇到的问题,八成集中在三个阶段:装环境时的命令找不到、跳转时的崩溃、布局写完之后效果和预期不符。这三类问题的排查路径完全不同,混在一起找会浪费时间。下面按现象、原因、解决三步整理五条高频问题,都是这份报告对应的实验里真实会遇到的。

5.1 SDK Manager 双击没反应或者一闪就退

现象是解压完 SDK 之后进 tools 目录双击 SDK Manager,窗口闪一下或者干脆不出现。原因通常是 JAVA_HOME 指向了 JRE 而不是 JDK,或者 JDK 路径里带空格、带中文,工具启动时解析路径失败。解决方式是先把 JAVA_HOME 改成 JDK 根目录,再确认路径里没有中文和空格;如果还是不行,在命令行里 cd 到 tools 目录,直接执行java -jar lib\archquery.jar之类的小工具看报什么错,把错误信息抄下来再搜,比盲试快得多。

另外还有一个隐性原因:系统里同时装了 32 位和 64 位的 Java,Path 里排在前面的那个和 SDK 工具位数不一致。命令行执行where java能列出所有 java.exe 的位置,按顺序确认哪个优先被调用。

5.2 Eclipse 里死活找不到 Android 选项

现象是 ADT 装完、Eclipse 也重启了,但 Window → Preferences 里没有 Android 这一项,新建工程也没有 Android Project。原因多半是 ADT 版本和 Eclipse 版本不匹配。实验环境用的是 Indigo 版本,搭配的 ADT 是那个年代的稳定版;换成高版本 Eclipse 去装老的 ADT,插件加载会失败但不会明确报错。

解决办法是先看 Help → About Eclipse 确认版本号,再去看安装时勾选的 Developer Tools 是否真的装上了,Help → Installation Details 里能看到已装插件列表。列表里有 ADT 但界面没变化,说明插件没加载起来,换匹配的版本重装。这里有个经验:插件装完一定要选 Restart Now 而不是 Later,选 Later 之后插件状态不确定,重启也未必生效。

5.3 点按钮直接崩,日志里是 ActivityNotFoundException

现象是主界面正常显示,一点按钮应用就退出,Logcat 里清清楚楚写着android.content.ActivityNotFoundException: Unable to find explicit activity class。原因只有一个:目标 Activity 没有在 AndroidManifest.xml 里登记。Android 的组件必须在清单里声明,不声明就等于不存在,编译器不会检查这一点。

解决是把<activity android:name=".OtherActivity" />补进 application 节点。补的时候注意包名层级,如果两个 Activity 在同一个包下,前面加点写相对名就行;跨包时必须写完整包名,否则系统会拼错路径。改完清单要重新安装应用,热部署不一定能把清单变更推上去。

5.4 传过去的值取出来是 null

现象是第二个界面的 TextView 空白,或者打印出来是 null。原因有三种:两端的 key 拼写不一致,比如发送端写name、接收端写Name;接收端用错了取值方式,比如发送端用 Bundle 打包,接收端却直接getIntent().getStringExtra("name");目标界面被桌面图标独立启动过,Intent 里压根没有 Bundle。

排查办法是在发送端和接收端各打一行日志,把完整 key 和值打出来对齐:

// 发送端 Log.i("PASS", "send key=name, value=" + testBundle.getString("name")); // 接收端 Bundle extra = getIntent().getExtras(); Log.i("PASS", "recv bundle=" + (extra == null ? "null" : extra.toString()));

两行日志一对比,问题立刻定位。我一般的习惯是把 key 抽成常量,比如public static final String KEY_NAME = "name";,两端引用同一个常量,拼写错误从源头消失,这算是血泪经验,比事后排查省事得多。

5.5 布局写完和效果图对不上

现象有三种:等分的色块宽度不一致、表格的列没对齐、界面被顶出屏幕出现滚动条。对应的原因分别是子 View 的主方向尺寸写了 wrap_content、TableRow 被换成普通 LinearLayout、某一层容器的高度写了固定值导致总高超出屏幕。

解决办法第一条是把参与等分的子 View 主方向尺寸统一改成 0dp,让 weight 说了算;第二条是检查 TableLayout 的直接子节点是不是 TableRow,不是的话列对齐逻辑不会生效;第三条是把外层容器的高度改成 fill_parent,中间段用 0dp 加 weight,整体比例就稳了。改完在模拟器里横竖屏各转一次,能转出不少隐藏问题。

6. 验收与答辩:怎么把四个实验截图变成一份能过关的报告

实验报告的交付物不只是代码,还包括截图、源码清单和一段能讲清楚的说明。很多人代码跑通了,报告还是被扣分,问题出在截图和源码对不上、答辩时说不清某一行代码的作用。这一章把验收环节需要准备的东西列成可核对的清单。

6.1 截图与源码的对应关系

四个实验要求提交的图不是随手截的,每一张都要能和源码文件对应上。可以按下面这张表逐项核对,缺哪一项补哪一项:

实验必交截图对应源码或配置文件验收要点
实验一JDK 安装目录、环境变量窗口、test.java 运行结果test.java运行结果里能看到 java.home 路径
实验一Eclipse 启动界面、SDK 目录、ADT 安装后界面AndroidManifest 无需Preferences 里 SDK Location 已填
实验二主界面、跳转后的第二界面MainActivity.java、OtherActivity.java、两个布局清单里两个 activity 都已登记
实验二第二界面显示“中国”TextMainActivity.java、SecondActivity.java两端 key 一致,接收端有判空
实验三三段式布局效果图MyLayout.xml三段等分,色块宽度一致
实验三多输入框对话框及点击后的输出dialog_input.xml、MainActivity.java取值用的是 dialogView.findViewById

截图时把模拟器的时间栏和工程名一起截进去,比只截内容区更有说服力。源码贴进报告要保留缩进和注释,一行行挤在一起的代码老师基本不看。

6.2 答辩时最常被追问的几个点

Activity 从 A 跳到 B,生命周期怎么走,返回时又怎么走。Intent 显式和隐式的区别,实验里用的是哪一种。为什么用 Bundle 而不是直接 putExtra。layout_weight 的分配规则,为什么宽要写 0dp。对话框里的控件为什么不能用 Activity 的 findViewById 去找。这几个问题在报告正文里都能找到答案,提前对着源码念一遍,回答时就不会卡壳。

还有一类问题是关于细节的:@+id里的加号是什么意思、fill_parent和match_parent有什么区别、onCreate里super.onCreate不写会怎样。这些不属于难点,但答不上来会显得代码是抄的。

6.3 交之前走一遍自查

我的习惯是代码跑通之后不急着截图,先把工程关掉重新打开,从零跑一遍,因为缓存和热部署会掩盖一些配置问题;然后按实验顺序把每一张要交的图列在纸上,截一张划掉一张;最后把源码从工程目录里复制出来单独编译一次,确认没有依赖 IDE 自动生成的东西。

有一次赶时间跳过了重开这一步,交上去之后老师在自己的机器上一跑就崩,原因是清单文件里少了一个 activity 节点,本机因为已经装过旧版本没暴露出来。从那以后我每次交这类实验报告,都会先把应用卸载再重装一遍,再按清单逐项核对截图。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进