
简介这是《安卓移动应用开发习题答案》PDF文档面向正在学习安卓开发入门课程、需要课后练习与复习巩固的高校学生与自学者。资源仅1个PDF文件体积698KB内容按单元组织以填空题、选择题和简答题形式给出覆盖开发环境搭建、模拟器创建、Android Studio组成结构与基本操作、应用创建与运行、界面设计与资源应用等核心知识点的参考答案且对单元1至单元3中较易混淆的细节做了归类说明。除标准答案外还对四层架构作用、重要目录与文件职责、Activity窗口样式设置等要点做了参考解析适合对照教材进行阶段性自测和查漏补缺。目前已有2849人学习下载可配合《基于安卓Studio的移动应用开发任务驱动教程》使用帮助学习者快速检验对基础概念和操作流程的掌握程度。1. 从 Android 四层架构看开发环境到底要装什么刚接触 Android 移动应用开发的人往往第一件事就是下载 Android Studio然后卡在 SDK 路径、模拟器启动、Gradle 同步这三座大山上。如果你翻过这份《Android 移动应用开发习题答案》会发现单元 1 反复在问四层架构和 JDK、SDK、ADT 的安装顺序——这不是考完就忘的填空题而是帮你把「装什么、为什么装、装完怎么验证」串成一条线。我见过不少转行做 Android 的同事环境装了三遍才搞明白 Android Studio 里其实内置了 JRE而 SDK 的 platform-tools 才是 adb 命令的真正来源。这篇文章我会按这套习题的骨架把环境搭建、Activity 启动模式、布局映射、数据存储、后台通信到媒体播放的完整知识链拆开每个环节都给出可复现的代码和解法。适合正在准备 Android 课程考试、或者刚入职需要快速补全四大组件知识的人。2. Activity 生命周期与四种启动模式任务栈里发生的事2.1 生命周期回调的调用顺序与状态判定Activity 的生命周期是面试和考试都逃不掉的点。习题里有道选择题问「Activity 从启动到关闭不会执行哪个方法」答案选的是onRestart()因为从首次启动到销毁正常的调用链是onCreate() - onStart() - onResume() - onPause() - onStop() - onDestroy()onRestart()只在「停止态重新可见」时才会触发。我用一个实际场景说明用户按下 Home 键Activity 走onPause() - onStop()再点回应用走onRestart() - onStart() - onResume()。但如果是被AlertDialog盖住一部分此时 Activity 仍然可见、只是失去焦点所以只走onPause()这就是习题里「被 AlertDialog 覆盖后处于暂停状态」的判定逻辑。实际开发中onPause()里不要做耗时操作因为新 Activity 的onResume()要等它返回才能执行。我一般把数据保存放在onStop()把资源释放放在onDestroy()Override protected void onStop() { super.onStop(); // 保存草稿箱数据此时界面不可见但进程仍在 SharedPreferences sp getSharedPreferences(draft, MODE_PRIVATE); sp.edit().putString(content, editText.getText().toString()).apply(); } Override protected void onDestroy() { super.onDestroy(); // 解注册广播、释放Bitmap等重资源 unregisterReceiver(receiver); }参数说明getSharedPreferences的第一个参数是文件名第二个MODE_PRIVATE表示仅本应用可读写apply()是异步落盘commit()是同步落盘——主线程里优先用apply()避免卡 UI。2.2 四种启动模式任务栈的出入栈规则习题里把四种启动模式列为简答题说明这是必须背清的知识点。standard模式每次启动都实例化不管栈里有没有singleTop只在「目标 Activity 已在栈顶」时复用否则新建singleTask保证栈内只有一份启动时会把目标 Activity 之上的所有页面出栈singleInstance直接开新任务栈适合需要全局唯一的页面。启动模式是否新建实例典型场景任务栈行为standard每次都是新实例普通页面跳转压入原任务栈singleTop栈顶存在时复用通知栏跳转、搜索页复用栈顶实例singleTask栈中存在则复用主页面、首页清除其上所有页面singleInstance全局唯一来电页面、播放器单独任务栈singleTask有个坑启动时会把目标 Activity 以上的页面全部onDestroy()如果这些页面有未保存状态用户返回时会丢数据。解决办法是启动前先把需要保留的数据写到持久化层或者在onNewIntent()里重新读取。2.3 Intent 属性与 startActivityForResult 传值习题里列出 Intent 的重要属性Component、Action、Data、Category、Type、Extra。显示 Intent 用 Component 指定类名隐式 Intent 用 Action Data Category 让系统匹配。跨页面回传数据传统写法是// 页面A启动页面B并等待返回值 Intent intent new Intent(this, SecondActivity.class); intent.putExtra(user_id, 1001); startActivityForResult(intent, REQUEST_CODE); // 页面B处理完业务后回传 Intent result new Intent(); result.putExtra(result, 操作成功); setResult(RESULT_OK, result); finish(); // 页面A接收结果 Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); if (requestCode REQUEST_CODE resultCode RESULT_OK) { String msg data.getStringExtra(result); Toast.makeText(this, msg, Toast.LENGTH_SHORT).show(); } }逻辑说明startActivityForResult两个参数分别是 Intent 和请求码请求码用来区分多个页面的回传setResult必须配合finish()才能触发页面 A 的onActivityResult。从习题的角度要记住第 2 个参数是请求码答案选 C。3. 布局与控件从 XML 属性到 findViewById 的完整映射3.1 五种布局的定位逻辑与适用场景习题把五种布局的特点列为简答其中绝对布局明确写了「不适合多种分辨率的屏幕需要慎用」。我实际项目中几乎只用三种线性布局做单方向排列、相对布局做整体骨架、约束布局做复杂页面。表格布局用TableRow控制行适合表单类界面框架布局的特点是子控件按创建顺序在左上角堆叠我一般用它做「底部图标 角标红点」这种叠加效果。相对布局的属性是考试重点android:layout_alignParentLeft表示「和父容器左对齐」android:layout_toLeftOf表示「在指定控件左边」android:layout_alignRight表示「右边缘对齐指定控件」。习题里问「是否跟父布局底部对齐」对应的就是android:layout_alignParentBottom。这里容易混淆的是align系和to系alignParentBottom是自身底部贴父容器底部above/below才是相对兄弟控件定位。3.2 id 与 id 的语义差别习题里有一道简答题id/username和id/username有什么区别。简单说id是在R.java中新建一个 id 资源id是引用已有的 id 资源。用id定义一个控件后其他控件引用它必须用id如果反复用id引用同一个名字编译器的行为是「如果已存在就当作引用」但规范写法是定义用id引用用id。在代码里获取控件所有布局和控件的 id 最终都会映射到R.id.xxxpublic class LoginActivity extends AppCompatActivity { private EditText etUsername; private Button btnLogin; Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_login); // R.id.btnLogin 对应布局文件中 id/btnLogin 的映射 etUsername findViewById(R.id.et_username); btnLogin findViewById(R.id.btn_login); btnLogin.setOnClickListener(v - { String name etUsername.getText().toString().trim(); if (name.isEmpty()) { etUsername.setError(用户名不能为空); } else { Intent intent new Intent(LoginActivity.this, HomeActivity.class); intent.putExtra(username, name); startActivity(intent); } }); } }说明findViewById返回的是View类型赋值给EditText变量时编译器会做类型检查如果类型不匹配会直接报错。setOnClickListener接收一个View.OnClickListener接口Lambda 写法本质就是匿名内部类的语法糖。习题里说「所有控件无一例外都继承自 View 类」所以findViewById才能统一返回View再向下转型。3.3 进度条、图片显示与 inputType 细节习题第 3 单元选择题里提到了进度条需要调用的方法、显示图片用 ImageView。实际中ProgressBar有两个进度获取方法getProgress()返回当前进度值getMax()返回最大值。图片显示除了 XML 里用android:src代码里可以用imageView.setImageResource(R.drawable.ic_launcher)也可以用 Bitmap 方式动态加载。EditText的android:inputType属性决定键盘类型inputTypenumber弹数字键盘inputTypetextPassword弹密码键盘并掩盖输入内容——这是习题里「inputtype 属性可以指定输入文本时的键盘特征」的具体含义。4. 数据存储四件套与 ContentProvider 的访问边界4.1 SharedPreferences 的存储机制与 Editor 提交习题里明确强调 SharedPreferences 本身没有写文件的能力必须通过Editor对象操作。理由是SharedPreferences实例只负责读取和缓存修改数据需要拿到edit()返回的EditorputString/putInt先把数据放进内存的 Map 里再调commit()或apply()持久化。// 写入三步走缺一不可 SharedPreferences sp getSharedPreferences(login_config, MODE_PRIVATE); SharedPreferences.Editor editor sp.edit(); editor.putString(token, abc123); editor.putBoolean(is_login, true); editor.apply(); // 异步落盘返回值是 void // 读取直接 get 方法带默认值 String token sp.getString(token, ); boolean isLogin sp.getBoolean(is_login, false);参数说明getSharedPreferences的第一个参数是文件名同名的 SP 文件全局共享MODE_PRIVATE是默认私有模式文件存放在/data/data/包名/shared_prefs/下。apply()会立刻更新内存缓存磁盘写入是异步的而commit()是同步阻塞写并返回 boolean主线程慎用。4.2 SQLiteSQLiteOpenHelper 的创建与数据操作习题里 SQLite 的创建过程答案很明确先继承SQLiteOpenHelper重写onCreate()创建表再调用子类对象的getWritableDatabase()获得可读写数据库对象。需要注意onCreate()只在数据库第一次创建时执行后续版本升级要写在onUpgrade()里CREATE TABLE IF NOT EXISTS user ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, score INTEGER DEFAULT 0 )public class DBHelper extends SQLiteOpenHelper { public DBHelper(Context context) { // 参数上下文、数据库文件名、游标工厂默认null、版本号 super(context, app.db, null, 1); } Override public void onCreate(SQLiteDatabase db) { db.execSQL(CREATE TABLE IF NOT EXISTS user ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, score INTEGER DEFAULT 0)); } Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // 版本升级时执行常见做法是先备份后缀改表再重建 db.execSQL(DROP TABLE IF EXISTS user); onCreate(db); } }数据增删改查有两种方式SQLiteDatabase的insert/delete/update/query方法封装了 SQL而execSQL只能执行非查询语句返回值是 void 而不是 Cursor。习题里专门考了这个点execSQL无法执行查询操作因为查询必须用rawQuery拿到Cursor。查询后必须关Cursor这是填空题答案明确写的DBHelper helper new DBHelper(context); SQLiteDatabase db helper.getReadableDatabase(); Cursor cursor db.rawQuery(SELECT * FROM user WHERE score ?, new String[]{60}); try { while (cursor.moveToNext()) { String name cursor.getString(cursor.getColumnIndexOrThrow(name)); int score cursor.getInt(cursor.getColumnIndexOrThrow(score)); } } finally { cursor.close(); // Cursor 必须关闭否则内存泄漏 db.close(); }4.3 ContentProvider 的 URI 访问边界习题里说 ContentProvider 统一了数据访问方式数据更安全原因是「只有通过特定 URI 才能访问」。这个 URI 的格式是content://authority/path/id比如系统联系人的 URI 是content://com.android.contacts/data/phones。外部应用拿不到你的数据库文件只能通过ContentResolver按 URI 访问。ContentResolver resolver getContentResolver(); Uri uri Uri.parse(content://com.example.provider/user); Cursor cursor resolver.query(uri, null, null, null, null); if (cursor ! null) { while (cursor.moveToNext()) { String name cursor.getString(cursor.getColumnIndex(name)); } cursor.close(); }这里有个安全细节4.0 以后系统要求ContentProvider显式声明android:exported如果是给第三方应用用的必须设为true同时建议加android:readPermission和android:writePermission做权限隔离。ContentObserver 则负责监听数据变化一旦 provider 里的数据被增删改onChange()会触发ContentResolver在回调查询哪些数据变了再进行操作——这就是习题 5 里「ContentProvider、ContentResolver、ContentObserver 三者关系」的完整链路。5. Service、BroadcastReceiver 与 Handler后台通信怎么串起来5.1 Service 两种启动方式的差异与回调时机习题里 Service 的考点集中在启动方式与生命周期方法。startService()启动的服务与调用者无关调用者关掉后服务继续在后台跑回调顺序是onCreate() - onStartCommand()多次启动只会重复走onStartCommand()bindService()启动的服务与调用者绑定调用者销毁时服务自动解绑回调解耦是onBind()多次绑定不会重复调用onBind()但会触发onServiceConnected。public class DownloadService extends Service { private static final String TAG DownloadService; Override public void onCreate() { super.onCreate(); // 服务只创建一次耗时初始化放这里 } Override public int onStartCommand(Intent intent, int flags, int startId) { // startService 方式启动才会回调这里可多次执行 String url intent.getStringExtra(url); // 这里不能直接做网络请求需要另开子线程 return START_STICKY; // 服务被系统杀死后尝试重建 } Override public IBinder onBind(Intent intent) { // bindService 方式才会回调这里返回 Binder 给调用者 return new DownloadBinder(); } class DownloadBinder extends Binder { public void startDownload(String url) { // 调用者拿到 Binder 后可以执行这个方法 } } }START_STICKY是onStartCommand的返回值表示服务被系统回收后如果还有未完成的 Intent系统会用 null 重建服务并重新回调适合下载这种需要恢复的任务START_NOT_STICKY则不会重建适合临时性任务。5.2 广播的两种注册方式与 4.0 后的限制广播注册分静态和动态两种。静态注册在AndroidManifest.xml里声明receiver应用没启动也能收到广播动态注册在代码里registerReceiver跟随组件的生命周期。习题里提到「4.0 以上的系统如果应用安装了但没打开过收到广播也不会启动应用」这是对静态广播的一个强烈限制很多低电耗广播和开机广播在国产 ROM 上还会进一步收紧。receiver android:name.BootReceiver android:exportedtrue intent-filter action android:nameandroid.intent.action.BOOT_COMPLETED / /intent-filter /receiver// 动态注册通常在 onStart 注册onStop 注销 private MyReceiver receiver new MyReceiver(); Override protected void onStart() { super.onStart(); IntentFilter filter new IntentFilter(); filter.addAction(com.example.MY_ACTION); registerReceiver(receiver, filter); } Override protected void onStop() { super.onStop(); unregisterReceiver(receiver); // 不注销会泄漏 Activity }有序广播用sendOrderedBroadcast发送接收者按优先级排队接收优先级高的可以调abortBroadcast()截断后续广播——习题里把abortBroadcast()和有序广播放在同一条填空题里就是这个意图。5.3 Handler 四对象从 sendMessage 到 handleMessage 的链路Handler 机制是 Android 异步通信的基石习题把 Message、Handler、MessageQueue、Looper 四个对象拆成了简答题。我的理解是子线程把数据封装进 Message通过 Handler 的sendMessage()投递消息进入主线程的 MessageQueue 排队Looper.loop() 死循环不断从队列取消息再交回 Handler 的handleMessage()处理。关键在于主线程默认自带 Looper子线程要自己Looper.prepare()Looper.loop()。// 主线程创建 Handler Handler handler new Handler(Looper.getMainLooper()) { Override public void handleMessage(Message msg) { // 这里运行在主线程可以直接更新UI switch (msg.what) { case 1: tvResult.setText(msg.obj.toString()); break; } } }; // 子线程里发送消息 new Thread(() - { String result 来自子线程的数据; Message msg Message.obtain(); msg.what 1; msg.obj result; handler.sendMessage(msg); }).start();说明Message.obtain()是推荐写法从消息池复用对象避免重复 new 造成内存抖动msg.what是 int 型标记msg.obj携带任意 Object。handler.sendMessage(msg)会把消息丢到与 Handler 绑定 Looper 的 MessageQueue 里如果是主线程 Looper消息就在主线程的队列里排队handleMessage在主线程执行所以能安全更新 UI。6. MediaPlayer 状态机与网络请求的线程切换细节6.1 MediaPlayer 生命周期从 Idle 到 Release 的状态转移习题里 MediaPlayer 的状态转移考得很细。new MediaPlayer()创建后处于 Idle 状态setDataSource()后进入 Initialized 状态然后必须进入 Prepared 状态才能start()。同步用prepare()异步用prepareAsync()异步期间处于 Preparing 状态准备好后系统回调OnPreparedListener。这些状态之间不能乱跳比如在 Started 状态下直接调prepare()会抛IllegalStateException。状态触发方法可以调用Idlenew / reset()setDataSourceInitializedsetDataSource()prepare / prepareAsyncPreparedprepare / prepareAsync 回调start / pause / stopStartedstart()pause / stopPausedpause()start / stopStoppedstop()prepare / prepareAsync 重新准备PlaybackCompleted播放完成start 重新播放6.2 prepare() 与 prepareAsync() 的取舍习题的简答题标准答案很实用prepareAsync()是异步加载执行完毕进入 Preparing 状态一般用于网络音频这类耗时场景prepare()是同步加载本地小文件可以用。网络视频用 MediaPlayer SurfaceView 时必须异步准备并在OnPreparedListener里调start()否则 ANR 风险非常大。MediaPlayer player new MediaPlayer(); try { player.setDataSource(https://example.com/audio.mp3); player.setAudioStreamType(AudioManager.STREAM_MUSIC); // 异步准备避免主线程卡死 player.prepareAsync(); player.setOnPreparedListener(mp - { mp.start(); // 准备完成后才开始播放 }); } catch (IOException e) { Log.e(MediaPlayer, 数据源出错, e); }参数说明setDataSource可以接收本地路径、Uri或FileDescriptor播放网络资源需要在 Manifest 里声明android.permission.INTERNET。OnPreparedListener回调时 MediaPlayer 已经进入 Prepared 状态此时才能调start()。释放资源时调release()之后此对象不能再使用。6.3 HttpURLConnection 的网络请求与线程约束Android 4.0 以后主线程不允许执行网络请求否则直接抛NetworkOnMainThreadException。习题里讲了 HttpClient 的基本步骤但 HttpClient 在 API 23 之后被移除现在统一用 HttpURLConnection。标准做法是子线程请求 Handler 回传结果private void fetchData(String url) { new Thread(() - { String result ; try { URL httpUrl new URL(url); HttpURLConnection conn (HttpURLConnection) httpUrl.openConnection(); conn.setRequestMethod(GET); conn.setConnectTimeout(5000); conn.setReadTimeout(5000); InputStream is conn.getInputStream(); BufferedReader reader new BufferedReader(new InputStreamReader(is)); StringBuilder sb new StringBuilder(); String line; while ((line reader.readLine()) ! null) { sb.append(line); } result sb.toString(); reader.close(); conn.disconnect(); } catch (IOException e) { Log.e(HTTP, 请求失败, e); } finally { Message msg Message.obtain(); msg.what 2; msg.obj result; handler.sendMessage(msg); // 回主线程更新UI } }).start(); }说明setConnectTimeout是建立连接的超时setReadTimeout是读数据的超时单位都是毫秒。GET 请求直接拼参数在 URL 上POST 需要setDoOutput(true)再往getOutputStream()写入请求体。返回的InputStream必须及时读取并关闭连接用完要disconnect()释放底层 Socket。子线程执行完一定要通过 Handler 切到主线程更新 UI直接在线程里操作 View 会报CalledFromWrongThreadException。本文还有配套的精品资源点击获取