ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Android App开发全链路:环境搭建、Activity与数据存储上架实战

Android App开发全链路:环境搭建、Activity与数据存储上架实战 Android App 开发这件事说难也难说简单也简单。我带过几个刚入行的朋友他们卡住的地方往往不是不会写 Java 或 Kotlin而是打开 Android Studio 之后两眼一抹黑——SDK 装哪个、Gradle 报红怎么办、模拟器跑得比蜗牛还慢、写好的界面在真机上直接崩。这些问题的共同点是官方文档都写了但没人告诉你哪些是必须要懂的哪些可以先放着不管。这篇内容我想做的是把 Android App 开发的基础链路从头到尾捋一遍从装工具、认目录、写界面、存数据一直讲到打包上架和成本这笔账。适合两类人看一是完全没碰过移动端、想从零起步的新手二是做过一点前端或者后端、想快速把 Android 这块补起来的人。我不会只丢一堆名词每一步都会说清楚为什么这么做、不做会出什么问题以及我自己踩过的那些坑。1. 环境搭建不是一路下一步几个选择会决定后面顺不顺很多人把装 Android Studio 当成装普通软件点完下一步就以为完事了结果第一个工程就卡在 Sync 上。环境这一关的核心不是装没装上而是工具链版本搭不搭。1.1 装 Android Studio 之前先确认 JDK 这件事Android Studio 现在自带了一个 JetBrains Runtime简称 JBR里面已经包含了 JDK所以绝大多数情况下你不需要单独去装 JDK。但这里有个容易翻车的点如果你电脑上早就装了别的 JDK而且JAVA_HOME指向了它Gradle 构建时可能会用错版本报出类似Unsupported class file major version的错误。我一般的做法是先不去动系统里的环境变量直接让 Android Studio 用它自带的 JBR。在设置里找到Build, Execution, Deployment Build Tools Gradle把 Gradle JDK 明确指到 IDE 自带的那个版本。这样能避开八成的明明装好了却编译不过的问题。如果你的项目确实需要指定某个 JDK 版本比如团队要求用 17那就统一在 Gradle JDK 这一栏设置而不是去改系统的JAVA_HOME。系统级的改动会影响其他软件而项目级的设置只影响这一个工程出问题了也好回退。1.2 SDK 管理器里哪些组件是必装哪些可以缓一缓打开 SDK Manager里面密密麻麻一堆组件新手很容易全勾上结果硬盘吃紧、下载半天。我的建议是按需装下面这张表是我这几年总结下来的取舍组件要不要装说明Android SDK Platform对应 API 级别必装至少装一个你目标用户覆盖最广的版本比如 API 34SDK Build-Tools必装打包和编译要用一般跟 Platform 版本对应SDK Platform-Tools必装里面含 adb、fastboot真机调试全靠它Android Emulator看情况要在电脑上跑模拟器就必须装System Image看情况模拟器用的系统镜像按 CPU 架构选NDK / CMake缓一缓只有做原生 C/C 开发、接第三方 so 库才需要Android SDK Command-line Tools建议装命令行构建、CI 上跑会用到重点说下 System Image。模拟器的镜像分 x86_64 和 arm64 两类Intel 或 AMD 的电脑选 x86_64跑起来会快得多Apple 芯片的 Mac 就得选 arm64。选错了不是不能跑而是慢到你怀疑人生——这一点后面还会细说。1.3 模拟器和真机什么时候必须插上数据线模拟器方便但有两个绕不开的短板。一是性能尤其是图形渲染和一些底层硬件相关的功能模拟器上表现和真机差得远二是某些功能只有真机才有比如打电话、发短信、传感器加速度计、陀螺仪、光线传感器、指纹、部分蓝牙协议。做基础开发的时候模拟器足够用了界面布局、页面跳转、数据存储这些都没问题。但只要你碰摄像头、定位精度、性能优化就老老实实上真机。真机调试有个新手老忘的步骤开启开发者选项。进设置找到关于手机连续点七次版本号开发者选项才会冒出来然后在里面打开USB 调试。插上数据线之后手机上会弹一个授权框勾选一律允许再确定不然 adb 是看不到设备的。注意如果插上电脑后 adb 一直显示device offline八成是数据线只支持充电不支持数据传输。换一根原装线或者换一个 USB 接口问题往往就解决了。2. 工程目录扫一遍每个文件夹都不是随便放的新建一个 Empty Activity 工程之后左侧的项目面板展开你会看到一大堆目录。很多人写了大半年代码还是没搞清楚res和assets的区别也不知道AndroidManifest.xml为什么这么重要。这一节把结构讲透。2.1 src/main 下的三兄弟java、res、assetssrc/main是主源码目录里面最常打交道的是三块java或 kotlin放代码。你的 Activity、自定义 View、工具类都在这里。包名一般跟应用 ID 对应倒过来写比如com.example.myapp。res放资源。资源的特点是它们会被编译并且可以通过R.xxx在代码里引用。常见的子目录有layout布局、drawable图片和图形、values字符串、颜色、尺寸、主题、mipmap应用图标。assets放原始文件。这里的文件不会被编译也不会生成资源 ID需要通过AssetManager以流的方式读取。适合放一些大文件、预置的数据库、HTML 页面等。这里有个很实用的区别如果一张图片需要在布局里通过drawable/xxx引用就放res/drawable如果是一个需要整段读取进来的文本模板或者离线网页就放assets。搞混了会导致图片能找到但文字读不出来这类莫名其妙的 bug。2.2 AndroidManifest.xml应用的身份证和门禁名单AndroidManifest.xml是整个应用的清单文件地位非常高。它主要管三件事第一声明应用的基本信息比如应用 ID、版本号、应用名称、图标。这些信息打包的时候会被写进 APK 里。第二注册四大组件。Activity、Service、BroadcastReceiver、ContentProvider 都必须在清单里登记漏了就直接崩报ActivityNotFoundException或者类似错误。新手最常见的崩溃之一就是新写了个 Activity 却忘了注册。第三声明权限。应用要访问网络、读写存储、使用摄像头都得在这里申请权限。从 Android 6.0 开始敏感权限除了在清单里声明运行时还得动态申请一次两个步骤缺一不可。manifest xmlns:androidhttp://schemas.android.com/apk/res/android uses-permission android:nameandroid.permission.INTERNET / uses-permission android:nameandroid.permission.CAMERA / application android:label我的应用 android:iconmipmap/ic_launcher activity android:name.MainActivity android:exportedtrue intent-filter action android:nameandroid.intent.action.MAIN / category android:nameandroid.intent.category.LAUNCHER / /intent-filter /activity /application /manifest注意那个android:exported属性。从 Android 12 开始凡是带intent-filter的组件都必须显式声明这个属性否则装不上。带LAUNCHER的那个 Activity 通常要设成true因为它需要被桌面启动。2.3 build.gradle 的两个层级和三个 SDK 版本号现在新建工程默认用的是 Kotlin DSL文件名叫build.gradle.kts老一点的项目是build.gradle。不管哪种都分两个层级项目级根目录下配置整个项目共用的东西比如插件版本、仓库地址。模块级app 目录下配置这个具体模块比如依赖库、编译版本。模块级里最关键的三个版本号新手经常分不清compileSdk、minSdk、targetSdk。我用一个比喻来解释compileSdk是你写代码时用哪套 SDK 来编译决定你能调用哪些新 API。minSdk是你的应用最低支持到哪个系统版本决定有多少老设备能装你的应用。targetSdk是你针对哪个版本做过适配测试系统会按这个版本的规则来对待你的应用。举个实际例子如果minSdk设成 24就意味着 Android 7.0 以下的手机装不了。设得越低覆盖用户越多但你要处理的兼容问题也越多。我一般新项目设 24做企业内部分发的可以设 26 甚至更高因为设备是可控的。提示改targetSdk的时候要格外小心因为它往往伴随行为变更。比如高版本对后台服务、通知权限、存储访问的限制越来越严直接调高可能让原本正常的代码失效。3. Activity 从点击图标到显示界面这一路发生了什么Activity 是 Android 里最核心的组件一个 Activity 大致对应一个屏幕。理解它的生命周期是理解 Android 应用运行机制的第一步。很多人写代码时在这里踩坑是因为只记住了回调的名字不知道每个回调的实际意义。3.1 七个生命周期回调的真实执行顺序当你在桌面上点开一个应用图标代码层面发生的过程大致是系统创建 Activity 实例依次调用onCreate、onStart、onResume界面显示出来并可以交互。我把它整理成一张对照表左边是回调右边是它被触发的时机和典型用途回调触发时机通常做什么onCreateActivity 第一次创建加载布局、初始化变量、绑定数据onStart界面即将可见注册一些轻量监听onResume界面可见且可交互开始动画、恢复传感器、刷新数据onPause界面失去焦点没完全消失暂停动画、保存临时状态要快onStop界面完全不可见释放较重的资源、注销广播onDestroyActivity 被销毁彻底释放资源防止内存泄漏onRestart从停止状态重新回到前台重新加载被释放的数据真正需要记牢的是成对关系onCreate对应onDestroyonStart对应onStoponResume对应onPause。在这三对之间做资源的申请和释放是最稳妥的写法。3.2 onCreate 里别干重活那是有原因的新手很容易在onCreate里塞一堆耗时操作比如读大文件、请求网络数据、初始化大型对象。结果就是点开应用之后黑屏或者白屏几秒体验非常差。原因在于onCreate是在主线程UI 线程上执行的而主线程负责渲染界面和处理输入事件。你在它里面做耗时操作界面就没法及时绘制用户看到的就是卡住不动。正确的做法是onCreate里只做轻的初始化把布局设置好、控件找到、简单数据绑定完。网络请求和读数据库这类操作交给子线程或者协程去处理结果回来之后再更新界面。override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val tvTitle findViewByIdTextView(R.id.tv_title) // 耗时操作放到子线程主线程负责更新 UI lifecycleScope.launch(Dispatchers.IO) { val data loadDataFromDatabase() withContext(Dispatchers.Main) { tvTitle.text data } } }这段代码用到了lifecycleScope它会把协程的生命周期跟 Activity 绑定在一起。Activity 销毁了协程自动取消不会造成内存泄漏。这是现在比较推荐的做法。3.3 屏幕旋转和数据丢失这个坑几乎人人都踩把手机横过来界面重新布局看起来很正常。但你可能没注意到屏幕旋转默认会销毁当前 Activity 再重新创建一遍。也就是说onCreate又被执行了一次你存在成员变量里的数据全没了。假设你在一个输入框里填了字然后旋转屏幕字消失了这就是这个机制导致的。解决办法有两个方向第一用onSaveInstanceState保存状态。系统在销毁 Activity 之前会调用它你往Bundle里塞数据重建的时候在onCreate的参数里取出来override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putString(draft, editText.text.toString()) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val draft savedInstanceState?.getString(draft).orEmpty() editText.setText(draft) }第二用 ViewModel 来保存数据。ViewModel 的生命周期比 Activity 长转屏重建的时候它不会被销毁数据自然还在。这是目前最主流的方式尤其是配合界面数据比较多的时候。这两种方式不是互斥的。一般表单里临时输入的用onSaveInstanceState界面展示的业务数据用 ViewModel。4. 布局与列表新手最容易写出卡顿界面的地方界面写得好不好直接决定用户对应用的第一印象。这一节讲三个点布局用哪种、列表怎么写、耗时任务和进度条怎么配合。4.1 ConstraintLayout 的约束思维和控件不见了的问题现在新建工程默认的根布局是ConstraintLayout为什么不用早年的LinearLayout或者RelativeLayout关键在于嵌套层级。LinearLayout和RelativeLayout想实现复杂布局往往要一层套一层层级一深测量和绘制的时间就成倍增加。ConstraintLayout用约束关系来描述位置绝大多数界面可以做到一层搞定渲染效率高得多。约束的核心就是四句话控件要确定位置必须至少有一条水平约束和一条垂直约束。只给水平约束它会跑到顶上只给垂直约束它会跑到左边。新手画出来的控件不见了九成是约束给少了或者给冲突了。几个常用的约束属性TextView android:idid/tv_title android:layout_width0dp android:layout_heightwrap_content android:text标题 app:layout_constraintStart_toStartOfparent app:layout_constraintEnd_toEndOfparent app:layout_constraintTop_toTopOfparent /这里layout_width设成0dp配合左右两条约束效果等同于撑满父容器宽度减去边距也就是常说的 match_constraint。这是 ConstraintLayout 里非常常用的一个技巧比写match_parent更灵活因为它还能配合 bias 调整偏移比例。4.2 RecyclerView 的复用机制和错位、闪烁的根源列表是应用里最常见的界面形态而RecyclerView是官方推荐的列表控件。它性能好的原因在于复用屏幕上能显示 8 个条目它可能只创建 10 个左右的 item 视图滑动的时候把滑出屏幕的视图回收再拿给新进入屏幕的条目用。但这个机制带来了一个经典陷阱。因为视图是复用的如果你在onBindViewHolder里只对某些条件设置了控件的状态没在 else 分支里重置那复用的旧状态就会残留下来。表现出来就是明明这条数据不该显示红点结果它显示了。正确的写法是每条数据绑定的时候把所有可能变化的属性都显式设置一遍override fun onBindViewHolder(holder: MyHolder, position: Int) { val item dataList[position] holder.tvName.text item.name holder.ivIcon.visibility if (item.hasNew) View.VISIBLE else View.GONE holder.itemView.setBackgroundColor( if (item.highlight) Color.YELLOW else Color.TRANSPARENT ) }凡是if里面改了状态else里面一定要还原。这几乎是我每次做列表都会提醒自己的事。至于列表图片加载时闪烁通常是图片还没加载完占位图和应用的主题背景色不一致导致的。解决办法是给图片控件设置一个固定的占位颜色或者用成熟的图片加载库来处理缓存和过渡动画。4.3 进度条不动多半是线程问题进度条这个控件看着简单出问题的时候却很让人抓狂代码明明写了更新进度界面上就是一根不动的横线。根本原因还是回到主线程。子线程更新 UI 会抛异常或者被系统忽略。正确的方式是计算进度的逻辑放在子线程更新进度条的动作丢回主线程。lifecycleScope.launch { withContext(Dispatchers.IO) { for (i in 1..100) { Thread.sleep(50) withContext(Dispatchers.Main) { progressBar.progress i } } } }还有一种情况进度条显示的是不确定的状态indeterminate设为 true那它本来就是一格不停旋转的样式不会显示具体百分比。要用确定进度记得把这个属性关掉。提示如果用ProgressBar做加载提示记得在数据加载成功后把它隐藏visibility设为GONE同时把真实内容显示出来。忘记切换状态用户就会一直看到转圈。5. 数据存哪里从键值对到本地数据库的递进选择应用不可能只展示静态内容总得存点东西用户设置、登录状态、缓存数据、业务记录。Android 提供的存储方案有好几种选错会让代码越写越乱。5.1 SharedPreferences 的边界在哪SharedPreferences是最简单的存储方式本质上是一个键值对文件适合存少量简单的配置项比如是否首次启动、用户选的主题、开关状态。val sp getSharedPreferences(settings, MODE_PRIVATE) sp.edit().putBoolean(dark_mode, true).apply() val isDark sp.getBoolean(dark_mode, false)它的优点是用起来快、简单。缺点是全量加载进内存数据一多就吃内存而且不支持复杂查询。我的经验是条目数控制在几十条以内、结构简单的场景用它是合适的一旦涉及列表数据、条件查询就该换数据库了。还有一点容易被忽略apply()是异步写commit()是同步写并返回结果。日常用apply()就行只有在必须确认写入成功比如关键配置的时候才用commit()。5.2 Room 的三层结构和一次完整读写Room是官方在 SQLite 之上封装的一个数据库库它让写数据库变得像写普通方法一样。核心是三个部分Entity一个实体类对应一张表。DAO数据访问接口定义增删改查方法。Database数据库持有者把实体和 DAO 关联起来。看一个完整的例子Entity data class Note( PrimaryKey(autoGenerate true) val id: Long 0, val title: String, val content: String ) Dao interface NoteDao { Insert suspend fun insert(note: Note) Query(SELECT * FROM note ORDER BY id DESC) suspend fun getAll(): ListNote } Database(entities [Note::class], version 1) abstract class AppDatabase : RoomDatabase() { abstract fun noteDao(): NoteDao }DAO 方法加上suspend之后就可以直接在协程里调用不用自己开线程。这一点非常重要数据库操作是 IO 操作绝对不能放在主线程否则数据量一大界面就卡死。用的时候一般做成单例避免反复创建数据库连接val db Room.databaseBuilder( applicationContext, AppDatabase::class.java, app.db ).build()5.3 content:// 开头的 URI跨应用数据共享的门道有时候你会看到一个以content://开头的地址长得像网址但不是网址。这是 Android 里 ContentProvider 暴露出来的数据地址用来在应用之间共享数据。一个典型的组成部分是这样的content://作者标识/路径/具体ID。前面是协议中间是提供方的唯一标识后面是要访问的资源。我们自己开发时最常打交道的场景是把文件分享给别的应用。出于安全考虑从某个版本开始应用之间不能直接传文件路径了必须通过 FileProvider 生成一个content://的临时授权地址。配置流程大致是provider android:nameandroidx.core.content.FileProvider android:authoritiescom.example.myapp.fileprovider android:exportedfalse android:grantUriPermissionstrue meta-data android:nameandroid.support.FILE_PROVIDER_PATHS android:resourcexml/file_paths / /provider再配合一个res/xml/file_paths.xml声明可以暴露哪些目录。然后在代码里用FileProvider.getUriForFile()拿到 URI通过 Intent 传出去。这里最容易出错的是 authorities 写错、路径没配对报IllegalArgumentException: Failed to find configured root。排查的时候先确认 authorities 跟代码里用的一致再确认你要分享的文件确实在声明的目录范围内。注意FileProvider 暴露的目录要收窄只开放确实需要分享的子目录不要把整个存储根目录都放出去那是明显的安全隐患。6. 调试排错日志、抓包和真机联调的实战方法写代码的时间其实只占一半另一半是在跟 bug 打交道。这一节讲三个最常用的排错手段。6.1 Logcat 看得出来问题就解决一半Logcat是 Android 的日志面板崩溃信息、系统提示、你自己打印的日志都在这里。新手常见的困惑是信息太多刷屏刷到看不清。几个实用技巧一是按级别过滤日志分 Verbose、Debug、Info、Warn、Error平时看 Error 和 Warn 就够二是按包名过滤把 Logcat 顶部的进程选项切到你的应用其他无关日志就没了三是用关键字搜索比如搜崩溃异常的类名。打印日志的时候养成好习惯用Log.d(TAG, message)TAG用当前类名并且不要在主线程里循环打印大量日志那样会拖慢应用。崩溃日志要重点看第一行和Caused by那一段。第一行说明异常类型Caused by里往往藏着真正的根因比如使用了空对象、数组越界、找不到资源。6.2 抓包失败通常卡在这几个地方抓包是排查网络问题的利器用来确认请求到底发出去没有、发了什么、服务端返回了什么。新手第一次抓包经常抓不到东西原因集中在几个方面应用使用了加密协议如果服务器用的是双向认证或者证书绑定普通的抓包方式看不到明文。这是安全设计不是 bug遇到这种只能从日志或者服务端侧排查。抓包工具的证书没装到系统信任区普通应用装在用户区就行但有些应用对证书来源有校验。手机和电脑不在同一网络代理方式抓包需要两者在同一网段代理 IP 和端口设错连不上。应用走了非 HTTP 通道比如用了自定义的 Socket 协议HTTP 抓包工具自然看不到。我的建议是先排查自己的应用。在代码里加一层日志把请求的 URL、参数、响应码打出来能解决大部分到底请求成功没有的疑问。抓包是辅助手段不是唯一手段。6.3 adb 高频命令装进脑子里能省很多时间adb是跟设备通信的命令行工具藏了一大堆实用功能。下面这些是我日常用得最多的命令作用adb devices查看已连接设备排查认不到设备的第一站adb install xxx.apk安装应用到设备adb uninstall 包名卸载应用adb logcat在终端里看日志配合 grep 很好用adb shell进入设备的命令行环境adb shell pm clear 包名清空应用数据回到首次安装状态adb pull 路径把设备上的文件拉到电脑adb push 本地路径 设备路径把电脑文件推到设备adb shell pm clear这个命令我特别推荐。测试首次启动流程、登录流程图省事不用卸载重装一条命令就回到干净状态。7. 打包上架签名、混淆和成本这笔账代码写完了界面调好了接下来就是打包发布。这一关没有什么技术难点但流程细节多漏一步就得重来。7.1 签名和 release 构建别拿 debug 包去发布调试用的 debug 包用的是系统默认签名不能用于正式发布。要发布必须生成自己的签名文件keystore并且在模块级的构建配置里配置 release 的签名信息。生成签名可以在 Android Studio 的构建菜单里操作也可以直接用命令行。关键是这个签名文件一定要保存好并且记住密码。应用上架之后后续所有版本更新必须用同一个签名文件签名签名换了系统就不认为这是同一个应用只能当新应用重新上架。android { signingConfigs { create(release) { storeFile file(my-release-key.jks) storePassword 你的密码 keyAlias key0 keyPassword 你的密码 } } buildTypes { release { isMinifyEnabled true proguardFiles( getDefaultProguardFile(proguard-android-optimize.txt), proguard-rules.pro ) signingConfig signingConfigs.getByName(release) } } }isMinifyEnabled打开之后会做代码压缩和混淆包体会变小代码也会被重命名增加逆向难度。代价是有些反射用到的类、跟后端约定的实体类可能被混淆掉导致崩溃这时候要在proguard-rules.pro里把它们保留下来。我一般的做法是反射、序列化相关的类统一加保留规则第一次打包之后完整跑一遍主要功能确认没崩再上传。提示签名文件和密码不要提交到代码仓库。放在本地或者用构建环境变量注入这是底线。7.2 上架材料和审核退回的常见原因国内的应用分发渠道和前几年相比流程规范了很多。上架通常要准备的材料包括应用名称和图标、应用截图、功能简介、隐私政策链接、软著或者相关资质视渠道和品类而定。审核被退回的原因我总结下来集中在三类权限说明不充分申请了某个敏感权限但功能说明里没解释为什么需要会被质疑过度索取权限。原则是按需申请用不到就别声明。隐私政策缺失或不一致应用实际收集的信息跟隐私政策里写的对不上。这个要认真核对尤其是集成了第三方统计、推送服务之后它们也会收集信息要在政策里如实说明。内容或功能不符合渠道规范这个就不用多说了功能要正当、内容要合规。我的经验是第一次上架预留一周左右的缓冲时间因为来回修改和重新提审都需要时间。资料提前准备齐能省掉不少来回沟通。7.3 自己做一个应用到上架大概要花多少钱这是被问得最多的问题但答案的跨度非常大因为做一个应用的定义太模糊了。我把成本拆成几块说这样估算起来有依据。第一块是开发工具。Android Studio 和 SDK 都是免费的这块基本是零成本。需要花的可能是电脑性能如果做原生、跑模拟器一台内存 16G 起步的电脑体验会好很多。第二块是证书和资质。国内渠道上架一般会涉及软件著作权登记这个有官方渠道和代办渠道价格从几百到上千不等。签名证书本身不花钱自己生成即可。第三块是服务器和后端。如果应用需要账号、数据同步、云存储就得有后端。轻量的可以用云服务商的入门配置一年几百块也能跑起来业务量上来了成本自然水涨船高。第四块是人力。这才是真正的开销大头。一个功能完整、体验过得去的应用从设计、开发、测试到上架往往需要不止一个人、不止一个月。如果是外包报价会按功能点、页面数、复杂度来算。所以如果有人问开发一个应用要多少钱我会反问你要的是个能跑通流程的演示还是一个能承载真实用户的产品这两者的成本可能差十倍以上。想清楚定位再谈预算才有意义。关于学习路径我个人建议是先用模拟器把第一个能交互的小应用跑通别贪多。把这个过程中的每个报错都当成一次补课会发现基础的东西其实翻来覆去就那些。等你能独立做出一个带列表、能存数据、能联网的小应用再回头看那些框架和架构就水到渠成了。
返回列表