从大一到工作我前前后后把《第一行代码》翻了不下五遍。每次重读都有新收获尤其是书里关于Android架构、四大组件、权限、持久化、通知、异步和服务这几块内容几乎覆盖了日常开发90%以上的核心场景。我整理了一套学习笔记把我自己理解和踩过的坑都补了进去今天分享给正在学Android或者准备面试的朋友。这套笔记适合刚学完Java基础、准备入坑Android的初学者也适合做过一两个小项目但总觉得基础不扎实的开发者。我会把每个知识点拆开讲清楚“是什么、为什么、怎么用”再补一些书上没细说但实际开发一定会遇到的细节。1. Android整体架构先在大脑里建一张地图1.1 系统分层从Linux内核到应用层Android不是一个单纯的“操作系统”它是一整套软件堆栈。从下往上可以分为Linux内核层、硬件抽象层、Android运行时、系统框架层和应用层。Linux内核层是Android的地基负责进程管理、内存管理、驱动模型这些底层活。早期Android设备刷机折腾内核就是在这层动手。硬件抽象层HAL把硬件能力封装成统一接口让上层不用关心具体是骁龙还是麒麟的芯片写代码时调用的Camera、Sensor接口背后都是HAL在转接。Android运行时ART是应用真正运行的地方每个App跑在独立的ART实例里这就是沙盒机制的来源。系统框架层提供了四大组件、资源管理、视图系统、通知管理等核心API我们写代码时用的Activity、Service、ContentProvider这些类都在这一层。最上层才是我们自己写的应用。学习Android架构我最大的体会是不需要一开始就背全每一层的细节但一定要知道“我写的代码跑在哪个位置、调用的API是谁提供的”。这样遇到问题才能快速定位是代码逻辑的锅还是框架层的坑还是设备兼容性问题。1.2 为什么Android要这样分层分层设计本质上是为了隔离变化。硬件厂商可以只关注驱动和HAL层系统开发者专注框架层应用开发者只跟SDK打交道各司其职。对咱们开发者来说分层的直接影响是我们写的App默认是互相隔离的不能随便读别人的数据不能随便调别人的能力。这也是Android安全模型的基础。后面讲权限大家会更深刻地理解这一点。2. 四大组件全景Android应用的骨架2.1 四大组件各自扮演什么角色Android应用由四种基本组件构成Activity、Service、BroadcastReceiver、ContentProvider。可以用一个类比来记一个App就像一家餐厅。Activity是餐厅的大堂负责接待客人、展示菜单、跟客人交互。Service是后厨不直接面对客人但一直在后台备菜、洗盘子。BroadcastReceiver是餐厅的广播喇叭告诉大家“今天打折”“水管爆了要停业”。ContentProvider是仓库管理员其他餐厅想借食材得通过他来申请。四大组件的核心区别在于Activity必须有界面是用户唯一能直接看到和交互的组件。Service没有界面适合做后台耗时操作比如下载文件、播放音乐。BroadcastReceiver用来接收系统或应用发出的广播比如电量低、开机完成、网络变化。ContentProvider用来跨进程共享数据比如读取系统联系人、相册图片。2.2 Intent连接四大组件的纽带四大组件之间不是孤立存在的它们靠Intent意图来串联。Intent就像一个信封你可以在上面写清楚“我要干什么”“我要传给谁”“带了什么数据”。显式Intent直接指定目标组件的包名和类名常用于App内部跳转。隐式Intent不指定具体组件而是声明action和category让系统去找能处理的组件。比如你想调起系统相机只需要发一个action为IMAGE_CAPTURE的隐式Intent系统会自动匹配相机App。初学者最容易踩的坑是隐式Intent匹配不到组件直接崩溃。例如想调起浏览器action写错了或者设备上没有浏览器就会抛出ActivityNotFoundException。解决方案很简单调用前用resolveActivity()判断一下能不能匹配到。2.3 生命周期Android开发的“万恶之源”Activity和Service都有完整的生命周期这是Android面试必问、开发必踩坑的内容。Activity生命周期最常用的场景是onCreate里初始化布局和数据onPause里暂停正在播放的视频或动画onDestroy里释放资源、注销广播。我踩过的坑是在onStop里没有保存用户正在编辑的草稿内容结果应用被系统杀掉后用户辛苦输入的文字全丢了。解决方案是在onPause或onStop里及时把数据持久化Activity不可见不代表数据安全了。Service生命周期相对简单onCreate-onStartCommand如果被start启动 -onDestroy。需要注意onStartCommand的返回值START_STICKY表示服务被系统杀掉后如果没有新的Intent系统会重新创建服务并调用onStartCommand参数Intent为nullSTART_NOT_STICKY则不会重启。后台下载任务一般用START_STICKY更适合。3. Activity组件深入用户交互的第一线3.1 Activity的四种启动模式Activity启动模式在Android开发中非常重要设置不对会导致界面栈混乱、二次点击打开多个相同页面等问题。四种启动模式分别为standard、singleTop、singleTask、singleInstance。standard模式是默认的每启动一次就创建一个新实例放回任务栈顶。singleTop模式表示如果栈顶已经是该Activity实例则复用而不创建新的适合接收推送通知跳转的场景避免消息提示页堆叠太多。singleTask模式更激进如果栈中已有该Activity实例系统会把它上面的所有Activity全部出栈把这个实例调到栈顶适合App主页面。singleInstance模式则是全局单例整个系统只保留一个实例并且该实例有独立的任务栈适合电话拨号界面这种需要被多个应用共享的页面。配置方式很简单在AndroidManifest.xml中给activity标签添加android:launchMode属性即可。3.2 Activity之间的数据传递Activity跳转时传递数据最常用的方式是Intent附带的Extra字段。传值类型支持所有基本类型、String、序列化对象实现Serializable或Parcelable接口。Intent intent new Intent(this, SecondActivity.class); intent.putExtra(name, 小明); intent.putExtra(age, 18); startActivity(intent);接收方使用getIntent()拿到Intent再用getStringExtra()、getIntExtra()取出数据。从一个Activity返回数据给上一个Activity则要用startActivityForResult()现在推荐用新的Activity Result API。我建议新项目直接用Activity Result API代码更清晰不用重写onActivityResult回调的样板代码处理权限回调也更方便。3.3 状态保存与恢复Activity被系统回收前会调用onSaveInstanceState()你可以在这里保存界面的临时状态比如输入框内容、滚动条位置。恢复时在onCreate的savedInstanceState参数中读取或者重写onRestoreInstanceState()。值得注意onSaveInstanceState只在Activity“非主动”被销毁时调用比如旋转屏幕、系统内存不足回收后台Activity。如果是用户点击返回键退出或者代码里调用了finish()则不会调用因为此时系统认为界面状态不需要保留。屏幕旋转是初学者最容易忽略的坑。默认情况下旋转屏幕会销毁并重建当前Activity如果你在界面加载时做了网络请求旋转一下屏幕就会重复请求。解决方案有很多最常用的是在Manifest中给Activity加上android:configChangesorientation|screenSize或者用ViewModel把数据缓存起来配置变化时自动保存和恢复。4. Service组件后台任务的正确打开方式4.1 两种启动方式与区别Service可以通过两种方式启动对应两种不同的使用场景。startService启动的服务一旦启动就会在后台一直运行即使启动它的组件销毁了也不影响。通过stopService或Service内部调用stopSelf()来停止。适合下载、上传、播放音乐这类不依赖调用方的后台任务。bindService绑定的服务生命周期跟随绑定方。绑定方销毁时Service会自动解绑并销毁。适合需要跟Service交互数据的场景比如音乐播放器需要Activity控制播放暂停可以通过bind方式拿IBinder接口来调用方法。4.2 前台服务不被系统杀死的保障从Android 8.0开始系统对后台服务限制得很严格应用在后台时不能随意创建服务。解决方法是使用前台服务Foreground Service前台服务会一直在通知栏显示一条通知告知用户当前正在进行的任务。创建前台服务的固定步骤先创建通知渠道再创建通知然后调用startForeground()把服务提升为前台服务并在服务内持续更新进度。我还记得第一次写下载功能时加了前台服务后通知栏能显示下载进度这个体验对用户来说非常友好也能避免应用进程被回收。需要特别留意前台服务必须配套动态权限申请在Android 13及以上版本需要申请FOREGROUND_SERVICE权限同时不同类型的服务如dataSync、mediaPlayback还需要声明对应的前景服务类型。4.3 Service与子线程的关系Service默认运行在主线程不能直接做耗时操作否则会触发Application Not RespondingANR错误。正确的做法是在Service内部开启子线程处理耗时任务Override public int onStartCommand(Intent intent, int flags, int startId) { new Thread(new Runnable() { Override public void run() { // 执行耗时操作 } }).start(); return super.onStartCommand(intent, flags, startId); }但这样管理线程比较粗糙更推荐用线程池或协程来管理Service内的异步任务。我在项目里通常用Executors.newFixedThreadPool()创建一个固定大小的线程池避免频繁创建和销毁线程带来的开销。5. 权限系统从安装时授权到运行时授权5.1 权限级别分类Android权限分为三个级别普通权限normal、危险权限dangerous、签名权限signature。普通权限不涉及用户隐私系统自动授予比如访问网络的INTERNET权限、设置闹钟的SET_ALARM权限。危险权限涉及用户隐私数据比如读取联系人、定位、读取存储必须在运行时向用户申请授权。签名权限是应用间共享数据用的只有使用相同签名的应用才能获得。5.2 运行时权限机制Android 6.0引入了动态权限机制危险权限不仅要声明还要在代码里逐个申请。权限申请过程可以拆解成四步。第一步在Manifest中声明权限。第二步在代码里判断是否已授权使用ContextCompat.checkSelfPermission()。第三步如果未授权调用ActivityCompat.requestPermissions()发起申请。第四步重写onRequestPermissionsResult()回调处理用户的选择。权限组的概念也值得了解。系统把危险权限分成了几个权限组比如存储组包含READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE位置组包含三个定位权限。需要注意用户一旦授权了某权限组里的其中一个权限系统会自动授权同组的其他权限。如果用户拒绝授权应用无法使用该权限对应的功能需要引导用户手动到设置页打开。5.3 动态申请权限的常规写法private static final int REQUEST_CODE_LOCATION 1; if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) ! PackageManager.PERMISSION_GRANTED) { ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, REQUEST_CODE_LOCATION); } else { // 已有权限直接执行定位逻辑 startLocation(); } Override public void onRequestPermissionsResult(int requestCode, NonNull String[] permissions, NonNull int[] grantResults) { super.onRequestPermissionsResult(requestCode, permissions, grantResults); if (requestCode REQUEST_CODE_LOCATION) { if (grantResults.length 0 grantResults[0] PackageManager.PERMISSION_GRANTED) { startLocation(); } else { Toast.makeText(this, 需要定位权限才能继续, Toast.LENGTH_SHORT).show(); } } }另外如果用户勾选了“不再询问”再调用requestPermissions也不会弹窗此时要引导用户去系统设置页手动授权。通过Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)配合packageName可以跳到具体的应用详情页。5.4 存储权限的分水岭Android存储权限是改动最频繁的权限之一。Android 10及以下版本需要申请WRITE_EXTERNAL_STORAGE权限才能读写公共存储目录。Android 10引入了分区存储应用只能直接访问自己的专属目录访问公共目录需要通过MediaStore。Android 11进一步增强了分区存储的强制要求普通应用基本无法直接访问其他应用的文件夹。我建议新项目直接按分区存储来设计不要再依赖绝对路径。访问自己应用的内部存储目录filesDir、cacheDir完全不用权限这应该成为首选的存储方案。公共目录的文件用MediaStore插入一条记录再做读写代码复杂一点但没有隐私合规风险。6. 数据持久化让数据活过应用重启6.1 三种基本持久化方式Android提供了三种基础持久化方式SharedPreferences、文件存储、SQLite数据库。SharedPreferences适合存键值对形式的数据比如用户偏好设置、登录状态、语言选择。本质是一个XML文件读取时一次性加载到内存所以不适合存大量数据。文件存储适合保存二进制或文本数据比如日志文件、下载的图片。内部存储路径不需要权限外部存储需要考虑安全限制。SQLite是Android内置的轻量级关系型数据库适合结构化数据的增删改查比如消息列表、商品数据。SQLite没有独立的服务器进程是嵌入式数据库直接以文件形式存在应用目录下。6.2 SharedPreferences的正确用法SharedPreferences有两种获取方式getSharedPreferences(name, MODE_PRIVATE)可以指定文件名getPreferences(MODE_PRIVATE)使用Activity默认文件名。写入数据要借助Editor对象SharedPreferences prefs getSharedPreferences(settings, MODE_PRIVATE); prefs.edit().putString(username, 小明).putBoolean(remember, true).apply();需要特别注意commit()和apply()的区别commit()是同步写入磁盘会阻塞调用线程有返回值apply()是异步写入立即更新内存再定时写盘没有返回值。如果追求高频率写入性能选apply()如果要确保写入成功后再做后续操作用commit()。初学阶段我踩过一个大坑用getSharedPreferences传了MODE_MULTI_PROCESS以为可以跨进程实时同步实际效果并不理想。从Android N开始这个标志位就被废弃了多进程数据同步应该用ContentProvider或者其他跨进程通信方案。6.3 SQLite与Room的选择直接用SQLiteOpenHelper写数据库代码样板代码多而且容易出现关联表时SQL语句写错。Jetpack的Room就是在SQLite之上做了一层抽象编译期就能校验SQL语句的正确性配合LiveData和协程用起来非常顺手。Room的三要素Entity定义表结构Dao定义数据访问接口Database声明数据库版本和实体列表。建库和升级都通过注解完成不需要手动写CREATE TABLE和ALTER TABLE。如果你的项目还没有引入过多的依赖我更推荐直接用Room而不是原生SQLite。因为Room的数据类型安全、查询校验、协程集成、迁移工具都非常成熟写几个注解就能完成一套增删改查远比自己维护SQLiteOpenHelper省心。6.4 数据持久化的场景实践日常开发中共享偏好设置适合保存用户偏好和轻量状态文件存储适合保存图片缓存、音视频文件Room适合保存结构化业务数据。举一个实际的例子我做消息列表的时候用Room存消息记录收到新消息先写入数据库再刷新界面用SharedPreferences存会话草稿用户切走再切回来时从SharedPreferences取出来恢复输入框内容用文件存储保存下载的附件。三种方式各司其职数据就不容易丢。7. 通知Notification触达用户的官方通道7.1 通知渠道Android 8.0的强制改造Android 8.0开始所有通知都必须归属于某一个通知渠道NotificationChannel否则通知不会显示。渠道的作用是让用户可以对不同类型的通知进行细化管理。创建通知渠道的代码如下NotificationChannel channel new NotificationChannel( chat_channel, // 渠道ID 聊天消息, // 用户可见的渠道名称 NotificationManager.IMPORTANCE_HIGH // 重要程度 ); channel.setDescription(接收好友聊天消息); NotificationManager manager getSystemService(NotificationManager.class); manager.createNotificationChannel(channel);IMPORTANCE_HIGH会横幅提醒、有声音IMPORTANCE_DEFAULT只在通知栏显示和响一声IMPORTANCE_LOW不响铃IMPORTANCE_NONE通知栏都不显示。渠道一旦创建重要程度和名称就无法再修改所以渠道规划要提前想好。7.2 通知的完整创建流程创建通知需要用到NotificationCompat.Builder我们一步步完成从渠道构建到通知展示的完整流程再配合PendingIntent实现点击通知跳转。先说PendingIntent。它和Intent的区别在于Intent是立即执行的动作PendingIntent是包装好一个Intent等条件满足时再执行。PendingIntent.getActivity()包装一个启动Activity的Intent点击通知后就会跳到指定界面。Intent intent new Intent(this, ChatActivity.class); PendingIntent pendingIntent PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE); Notification notification new NotificationCompat.Builder(this, chat_channel) .setContentTitle(你有一条新消息) .setContentText(小白发来一条消息晚上一起吃饭吗) .setSmallIcon(R.drawable.ic_notification) .setContentIntent(pendingIntent) .setAutoCancel(true) .build(); NotificationManager manager getSystemService(NotificationManager.class); manager.notify(1, notification);这里要提醒一个常见坑从Android 12开始PendingIntent必须指定可变性标志。如果不需要系统帮你更新Intent里的附加数据用FLAG_IMMUTABLE如果需要跨应用更新或者Intent里带的数据允许被系统修改才考虑FLAG_MUTABLE。7.3 通知的进阶玩法通知栏不只是显示一行文字还可以放一个大图、进度条、长文本或者操作按钮。最典型的场景是下载进度通知先设置setProgress(100, progress, false)下载过程中反复调用notify()刷新进度下载完成后再调用manager.cancel(id)把通知移除。一个大坑是通知栏权限。Android 13把通知权限也改成了运行时权限需要在AndroidManifest中声明POST_NOTIFICATIONS并在代码里动态申请否则通知直接不显示。这个和权限章节是一套联动知识。8. 异步编程不卡顿的Android应用基础8.1 主线程不能卡原因是什么Android所有UI操作都必须在主线程执行而耗时操作不能放在主线程。原因很简单如果主线程阻塞超过5秒系统就会认为应用无响应弹出ANR弹窗严重时直接杀掉应用。于是异步编程成了Android开发者必须掌握的技能。需求就是耗时操作在子线程执行执行完切回主线程更新UI。8.2 Thread Handler最经典的异步方案Handler的机制可以概括为子线程通过sendMessage()把消息发送到MessageQueue主线程的Looper循环读取队列再交给handleMessage处理。这就是Android消息循环的核心原理。private static final int MSG_UPDATE_TEXT 1; private Handler handler new Handler(Looper.getMainLooper()) { Override public void handleMessage(NonNull Message msg) { if (msg.what MSG_UPDATE_TEXT) { textView.setText((String) msg.obj); } } }; new Thread(new Runnable() { Override public void run() { // 耗时操作 Message message new Message(); message.what MSG_UPDATE_TEXT; message.obj 加载完成; handler.sendMessage(message); } }).start();这里要提醒Handler一定要用Looper.getMainLooper()初始化否则handleMessage会跑在子线程更新UI直接崩溃。更简洁的做法是用runOnUiThread()或者View自带的post()方法其实底层都一样。8.3 从AsyncTask到协程异步方案的演进AsyncTask是早期官方推荐的轻量级异步方案封装了线程池和Handler。但它有很严重的问题生命周期不跟随Activity容易内存泄漏任务取消机制不完善doInBackground执行耗时太长会阻塞线程池。现在官方已经废弃AsyncTask。我建议新项目直接把异步主力放在Kotlin协程上。协程的核心优势是代码写起来像同步但执行是异步的不用担心回调地狱lifecycleScope.launch { val result withContext(Dispatchers.IO) { // 网络请求或数据库操作 apiService.getUserInfo() } textView.text result.name }这样写的好处是withContext(Dispatchers.IO)切换线程执行耗时操作执行完自动切回主线程更新UI代码顺序读下来就是行云流水的逻辑。如果你还在用Java写Android线程池是最稳的选择。Executors.newFixedThreadPool(4)创建一个固定线程数的线程池配合ExecutorService.submit(Callable)可以拿到Future来获取执行结果。切忌直接用new Thread()创建大量线程频繁创建线程的CPU开销和内存开销都很可观。8.4 异步与线程同步的关系异步不等于线程安全。多个子线程同时访问同一个变量、同一个数据库连接会出现数据竞争问题。Android主线程更新UI是定死的规则但子线程之间共享数据的保护要靠Synchronized、Lock或者ConcurrentHashMap这类并发容器。一个我踩过的坑下载列表用线程池并发下载多个文件同时写入同一个数据库表没有加锁导致SQLite报DatabaseException。后来我把写入操作统一串行化放到一个单线程的Executor中执行问题才解决。以上是我学习Android核心知识的一点心得。我把这些整理出来的原因是很多新手提到Android就觉得门槛高、内容杂其实真正的核心就这么多架构、四大组件、权限、持久化、通知、异步、服务把每一条吃透你就能解决开发中80%的问题。最后分享一个小技巧读《第一行代码》的时候不要只是看一定要自己动手做笔记和写Demo。我每学一个知识点都会写一个能运行的最小示例然后故意改点参数、制造点错误看看会有什么现象。很多Android机制的“反直觉”之处就是这样被记住的。 SEO 优化官网定制响应式建站教育培训建站