整理这篇文章的初衷
- 一篇文章解决所有Android面试开发学习的疑难杂症
跨端
如何实现Native与H5的交互?
- 通过webView的addJavaScriptInterface注入Java对象,允许H5调用Native方法
- 通过WebviewClient拦截URL使用eveluateJavaScript执行JS脚本,实现Native调用H5
混合开发的适用场景有哪些?
- 快速迭代,跨平台一致性
- 缺点:性能不好,原生依赖桥接文件
- 场景:电商、活动页面、实效性较强
Flutter如何与原生进行通信
- PlatformChannel(MessageChannel、Event Channel)
Flutter性能优化常见手段
- 减少widget重建、
- 列表使用listView.build懒加载
- 避免频繁的通信通道
如何实现安卓iOS双端UI的一致性
- 使用跨平台框架如Flutter统一UI
系统与底层
Binder机制
- 是Android系统实现跨进程通信(IPC)的核心机制
- Binder通过内存映射直接传输数据,省去两次拷贝过程,像快递员直接送货到你家仓库
Binder架构模型核心组件
- Binder驱动:作为进程间通信的
AMS
WMS
Native开发
HAL层
ROM定制经验
虚拟机(JVM) (JVM与字节码、JVM与类、JVM与实例、安卓平台虚拟机)
- 在硬件上运行JVM语言(可以编译成Java字节码的语言:Java、Groovy、Kotlin)
- 执行字节码命令
- 加载字节码中的class结构
- 分配和回收Java运行时的内存
- 基于栈的Java虚拟机,移植性好,可压缩体积
- 如何看待Android平台虚拟机是基于寄存器的呢?为什么不是基于栈呢?
- 为什么dex文件比class文件更适合移动端?
- ClassLoader的双亲委派模型(你能不能自己写一个Object类)
- 所有被new出来的实例,都是放在堆么?
- GC为什么会导致卡顿?
- 双重检测的单例,为什么还要加volatile关键字?
编程语言与跨平台技术
Flutter
Flutter的核心组件
- Widget:构建UI的基本元素
- Element:UI强度及其结构
- Render Object:负责渲染的对象
StatefulWidget和StatelessWidget区别是什么?
- Stateless Widget是无状态的
- Stateful Widget是有状态的,可以根据状态变化重新构建UI
- Stateful Widget的状态由对应的State类管理
如何在Flutter中进行布局
- Column
- Row
- Stack
- Container
- Expanded
Navigator是啥?如何使用?
- Flutter的路由管理组件,Push、Pop
什么是Flutter的Hot Reload 和 Hot Restart?
- 前者可以快速加载应用的代码更改,无需重启应用,保留当前的状态
- 后者重启整个应用并重新加载所有代码,但状态不会保留
Flutter是如何处理状态管理的?
- setState()简单的状态管理
- InheritedWidget:用于在Widget树中共享状态
- Provider:使用广泛的状态管理库,基于InheritedWidget
- Bloc/Riverpod/ModX:更复杂的状态管理解决方案
Flutter中如何实现网络请求
- http库
什么是Flutter的Future和 async/await
- Future是一种表示异步操作结果的对象
- 在Dart中可以使用async和wait关键字来简化异步编程
- async用来定义异步请求,awiat用于等待Future完成
uni-app
iOS
HermonyOS
Compose与传统View体系的区别?
- Compose用声明式编程,像乐高按图纸自动拼装;传统View是手动摆积木,改布局要拆了重搭
Kotlin Multiplatform
AR/VR在安卓的实现
Foldable设备的适配
深色模式、隐私权限的动态控制
- String的intern方法
- String的HashCode为什么是乘以31?
- HashMap的容量为什么一定是2的n次方?
- 为什么Java的数组不支持泛型?怎么在运行时获得泛型?
- 匿名内部类引用外部类的成员变量的时候,为什么一定要加final关键字呢?
- Java里面有闭包么?
- 都是编译成字节码,为什么Kotlin支持Java没有的特性?
安卓AI、机器人
ML Kit/Tensorflow Lite
AIGC 在代码生成和代码辅助中的作用
AI驱动的性能监控和Crash分析
大模型在端侧的应用潜力
- 模型需要量化压缩,使用GPU、NPU进行加速,且保证用户数据不出设备
如何设计高可用架构
- 集成Tensor或者Pytorch
implementation 'org.tensorflow:tensorflow-lite:2.7.0'
implementation 'org.tensorflow:tensorflow-lite-gpu:2.7.0' // 如果需要 GPU 支持
- 准备模型
converter = tf.lite.TFLiteConverter.from_keras_model(model)
tflite_model = converter.convert()
- 加载模型并进行推理
val tfliteModel = FileUtil.loadMappedFile(context, "model.tflite")
val tfliteInterpreter = Interpreter(tfliteModel)
// 创建输入/输出张量
val input = Array(1) { FloatArray(inputSize) }
val output = Array(1) { FloatArray(outputSize) }
// 进行推理
tfliteInterpreter.run(input, output)
- 创建输入输出张量,并进行推理
在安卓设备上进行模型优化的时候,通常会使用哪些方法
- 模型量化:减小模型大小和计算延迟
- 模型裁剪:使用TensorFlow Lite和 ONNX Runtime
- GPU加速
经典面试题
Activity、Window、View之间的关系?
- Activity负责界面展示、交互、业务逻辑处理
- Window是职能部门:View容器、用于管理View相关事宜
- 事件分发也是先交于Window
- View是Window的元素,是其具体呈现
SharePreference多线程使用会怎么样?如何保证跨进程使用的安全?
- 允许多个进程同时读取,但是从6.0开始已经废弃
- 不能保证一致性,需要使用同步机制
- 建议使用ContentProvider
- 使用synchronized关键字,确保同一时间只有一个线程访问
- apply()异步处理,减少对UI线程的阻塞
- kotlin直接可以使用@Synchronized注解
- ReentrantLock
- 使用单例模式,防止多个实例同时访问
RecyclerView和ListView区别
RV缓存比LV多了两层,可以更好的缓存View,RV可以实现局部刷新,性能更稳定
RV嵌套卡顿怎么解决
设置预加载数量,设置自带滑动冲突解决属性 rv.setHadFixedSize(true) rv.setNestedScrollingEnabled(false) 谷歌不推荐嵌套、寻找三方控件比如:ExpandableListView
View和SurfaceView的区别
- View基于主线程刷新UI、SurfaceView子线程又可以刷新UI
- View工作在主线程,SurfaceView工作在渲染线程
- View适合展示或者简单动画,SurfaceView更适合高性能复杂动画,常用语视频播放、相机、游戏开发,有双缓冲机制
String、StringBuffer、StringBuilder的区别
- String不可改变对象,一旦创建就不能修改。重新赋值就是重新new
- StringBuffer、StringBuilder创建了就可以修改
- StringBuilder执行效率高于StringBuffer,字符赋值少则使用StringBuffer,频繁则使用StringBuilder,当多个线程同步操作数据则使用StringBuffer
说一下wait和sleep的区别
- wait是Object的方法,wait是对象锁,锁定方法不让继续执行,当执行notify方法后就会继续执行
- sleep则是Thread的方法,sleep是让线程睡眠,让出CPU,结束后自动继续执行
一个APP中可以有多个进程么?说一下进程和线程的区别?
- 默认单进程,每个APP有一个主进程,由AndroidManifest.xml中的标签默认配置
- 创建多进程:通过在Manifest中为组件(如:Activity、Service),目的是隔离耗时操作或者敏感任务,避免主线程阻塞
- 利用多核CPU,但需要注意多进程间内存不共享,通信需要通过IPC(如AIDL、Messenger)
- iOS应用通常为单个进程(主进程),后台任务或者扩展(如widget、share Extension)以独立进程运行,但是属于不同的沙盒环境,通信需要通过系统接口(App Group、XPC)
跨进程通信(IPC)有几种方式
- Bundle:实现了Parcelable接口 ,可在不同组件(Activity、Service等)间传递数据。在跨进程通信的时候,能把数据存在Bundle中,然后通过Intent传递
- 文件共享:一个进程把数据写入文件,一个进程从文件中读取数据,适合对数据同步要求不高的场景(存在性能问题,如多个进程同时操作文件可能产生数据不一致的问题)
- Messenger:基于AIDL实现,是一种轻量级的跨进程通信方式,以Message为载体,实现进程间的单向通信(效率不高)
- AIDL(Android Interface Definition Language):安卓提供的一种接口定义语言,用于定义跨进程通信的接口。通过AIDL文件,系统会自动生成相应的Java代码,从而实现进程间的方法调用
- ContentProvider:安卓提供的一种标准的跨进程数据共享方式,用于在不同应用间共享数据。通过ContentProvider,可以对数据进行增删改查操作
- 打电话
- 广播
- 接口共享数据
- binder(通过binder,安卓能让应用程序与系统服务、其他应用以及自身的不同组件进行交互)
- socket:基于网络通信方式,通过TCP或者UDP协议实现跨进程通信,不同进程可作为客户端和服务端进行数据交互
所有被new出来的实例,都是被存放在堆里面么?
- 堆和栈的区别是什么? 堆依赖GC,栈不依赖,出栈就会被销毁
- 实例放在栈中的好处是什么?: 实例出栈就被销毁,不会依赖GC,没有额外开销
- 什么情况下实例适合放在栈中?:实例被返回到方法外部,方法出栈后,实例不能销毁;堆实例能被其他线程访问,但是栈是线程私有的
GC为什么会导致卡顿?
- GC线程导致工作线程停止(STW:GC线程执行的时候暂停一切用户线程)
- GC线程和用户线程并行,可能会导致回收不彻底、空指针异常(并行GC的脏实例问题)
- 堆调优:尽量减缓GC触发次数、尽量缩小GC范围、想办法让GC并行、并发 修改分代回收区域的大小
- 垃圾检测算法:引用计数法:问题:互相引用的问题;可达性分析法:
- 垃圾回收算法:
为什么dex文件比class文件更适合移动端?
- 内存有限
- 安装包体积不宜太大
- 频繁I/O操作会造成卡顿
为什么Parcelable的速度优于Serializable
- 序列化最重要用于传输和存储
- 将实例的状态转化为可以传输或者存储的形式(二进制、字符串都可以,只要能被写入磁盘)
- 将这种形式转化为实例则是反序列化
- 跨进程传输
- writeObject、readObject
点击APP图标的时候,系统都做了什么?
Activity是如何被显示在屏幕上的?
Android 为什么设计只有主线程更新UI
- 一般UI还是要保证同一时刻只有一个线程在更新,所以效率不会更高
- 多线程更新UI实现上会复杂一些,Java的内部人员发布过文章也说过这个几乎不可实现
- 从响应速度角度分析,单线程可以设计出更好的响应速度的api
- 单线程更新,也是一个被证明效果非常好的方案
子线程操作UI的几种方式
- Handler的post()方法
- View的post()方法(其实就是调用了Handler的post方法)
- Activity的runOnUiThread()方法(本质也是一样的,调用Handler的post方法)
- 在Runnable对象的run()方法里更新UI,效果完全等同于在handleMessage()方法中更新UI
Handler消息机制
- 用于线程间通信
- 特别是将任务从子线程切换到主线程处理UI
- Handler、Looper、Message Queue、Message
- Message Queue:负责存储和管理消息、消息队列需要线程安全,所以要通过锁和同步机制来保证入队和出队的原子性,使用 wait\notify来保证线程同步,当对队列为空时,Looper等待,有消息的时候唤醒
- Looper:负责从Message Queue中去消息并分发给对应的Handler,每个线程需要一个唯一的Looper,Looper.loop启动消息循环,不断调用Message Queue的next()方法获取消息
- Handler:需要持有Message Queue和Looper的引用,这样才能发送消息到队列,发送消息的方法如: post、 sendMessage等,最终都会调用sendMessageAtTime,将消息放入队列
- 消息处理:当Looper取出消息之后,会调用msg.target.dispatchMessage(),这里的target是发送消息的Handler,然后分发给handleMessage或者Runnable回调,这需要Handler实现dispatchMessage方法,判断是否有回调或者重写的方法
- 线程间通信需要保证Handler的处理方法在目标线程执行,这通过将Looper和Message Queue与目标线程绑定来实现,主线程在启动的时候已经初始化了Looper,所以Handler在主线程中创建的时候会自动绑定,子线程需要手动调用Looper.prepare和loop()
如果要你设计,怎么设计?
- 消息队列(Message Queue)
- 循环器(Looper)
- 处理器(Handler)
- 消息分发 Looper取出消息,调用msg.target.dispatchMessage()
- 同步机制:MessageQueue的enqueue和next操作需要加锁,防止多线程竞争
- 延迟计算:使用系统启动时间(SystemClock.uptimeMills)避免时钟偏差
- 对象复用:通过Message.obtain()实现消息对象池,减少内存分配
// 消息队列
class MessageQueue {
private Queue<Message> queue;
public void enqueueMessage(Message msg) { /* 同步入队 */ }
public Message next() { /* 同步出队+阻塞控制 */ }
}
// Looper
class Looper {
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<>();
MessageQueue mQueue;
static void prepare() { sThreadLocal.set(new Looper()); }
static void loop() { while(true){ mQueue.next().target.dispatchMessage(); } }
}
// Handler
class Handler {
final Looper mLooper;
final MessageQueue mQueue;
public Handler() {
mLooper = Looper.myLooper();
mQueue = mLooper.mQueue;
}
public void dispatchMessage(Message msg) {
if(msg.callback != null) msg.callback.run();
else handleMessage(msg);
}
public void sendMessage(Message msg) {
msg.target = this;
mQueue.enqueueMessage(msg);
}
}
```<sub index="6" url="https://juejin.cn/post/6844903488896385031" title="自己动手撸一个Handler - 稀土掘金" snippet="四、实现原理图 1、 Handler的实现由于Handler主要负责发送和处理消息,那我们主要实现它的sendMessage、sendMessage、dispatchMessage三个方法,来处理消息的发送和接收:public class Handler { //消息队列 MessageQueue mQueue; //Looper Looper mLooper; public Handler { mLooper = Looper.myLooper; if (mLooper == null) { throw new RuntimeException( "Can't create handler inside thread that has not called Looper.prepare"); } mQueue = mLooper.mQueue; } public final void sendMessage(Message msg){ MessageQueue queue = mQueue; if (queue != null) { msg.target = this; queue.enqueueMessage(msg); }else { RuntimeException e = new RuntimeException( this + " sendMessage called with no mQueue"); throw e; } } /** * Subclasses must implement this to receive messages. */ public void handleMessage(Message msg) { } /** * Handle system messages here. */ public void dispatchMessage(Message msg) { handleMessage(msg); } } 我们在Handler的构造函数中获取当前线程对应的looper,并取出Looper中对应的消息队列保存在成员变量中。sendMessage方法中我们给Message的target变量赋值为this,也就是表明了Message是由当前的Handler来负责处理的,之后调用enqueueMessage方法将消息存入消息队列中。"></sub><sub index="8" url="https://juejin.cn/post/6917464585221963790" title="自己写一个Handler 机制? - Android - 稀土掘金" snippet="以及一个最重要的目标,如何保证所有的消息处理都在目标线程中执行呢?设计思路首先是第一个目标,如何处理?这简单直接利用一个 Handler 类,里面包含处理 Message 的函数即可。那么第二个目标又如何实现呢?既然 Handler 作为消息的处理类,那么 Handler 类的实例化对象的消息处理肯定的位于目标线程中,其次 Lopper 是需要直接依赖 MessageQueue 的,直接将 MessageQueue 的初始化交给 Looper 就好,最后呢就只需要思考 Looper 的设计了。首先 Looper 中有一个死循环操作,那么单个线程中只能有一个 Looper (有多个也没啥用),然后想想 Hnadler 和 Looper 之间的关系,Looper 获取消息而 Handler 处理消息,两者能不能处于不同的线程呢?,除此之外也没啥好设计的了,退出机制?要想的话加一个就行,没消息就退出,或者延时退出都行。处理发送消息发送消息的目的就是向 MessageQueue 中添加一条 msg,这里我们需要考虑的问题就是如何处理好多线程之间的交互问题,我们在这里使用 Java 线程的 wait / notify 机制实现(早期 android 系统中也是利用这个机制实现),Looper 在死循环获取消息时进行加锁,获取到的消息为空时就使用 wait 进行等待,当有消息添加时notify唤醒 Looper 线程处理消息。至此,Handler 和 Looper 的重新改造的模样如下:// Handler public class Handler { /** * 处理 Message 的方法,需要继承者实现。"></sub>
该设计实现了跨线程消息传递的核心功能,通过绑定线程本地Looper保证消息处理在目标线程执行,满足基础的异步通信需求。
- 典型案例一
public class Activity extends android.app.Activity {
private Handler mHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
System.out.println(msg.what);
}
};
@Override
public void onCreate(Bundle savedInstanceState, PersistableBundle persistentState) {
super.onCreate(savedInstanceState, persistentState);
setContentView(R.layout.activity_main);
new Thread(new Runnable() {
@Override
public void run() {
...............耗时操作
Message message = Message.obtain();
message.what = 1;
mHandler.sendMessage(message);
}
}).start();
}
}
- 典型案例二
public class MainActivity extends Activity {
private Handler handler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
handler = new Handler();
new Thread(new Runnable() {
@Override
public void run() {
handler.post(new Runnable() {
@Override
public void run() {
// 在这里进行UI操作
}
});
}
}).start();
}
}
- 在子线程中,进行耗时操作,执行完操作后,发送消息,通知主线程更新UI。这便是消息机制的典型应用场景
- 我们通常只会接触到Handler和Message来完成消息机制,其实内部还有两大助手来共同完成消息传递 Message: 需要传递的消息,可以传递数据;
MessageQueue: 消息队列,但是它的内部实现并不是用的队列,实际上是通过一个单链表的数据结构来维护消息列表,因为单链表在插入和删除上比较有优势。主要功能向消息池投递消息(MessageQueue.enqueueMessage)和取走消息池的消息(MessageQueue.next);
Handler: 消息辅助类,主要功能向消息池发送各种消息事件(Handler.sendMessage)和处理相应消息事件(Handler.handleMessage);
Looper: 不断循环执行(Looper.loop),从MessageQueue中读取消息,按分发机制将消息分发给目标处理者。消息来了就唤醒,消息发出去之后,就挂起
四大组件
Activity
- 负责界面展示和交互
Service
- 在后台执行长时间任务的组件,不直接与用户交互,主要处理耗时操作(网络请求、文件下载、音乐播放)
- 前台服务和后台服务(Android8.0之后对后台服务有严格的限制)
- 启动方式:startService(启动后独立运行)bindService(与调用者绑定,跟随调用者销毁而停止)启动
- 需要在AndroidManifest.xml中注册
BoardcastReceiver
- 用于接收来自系统应用的广播消息(开机完成、网络状态变化、电池电量)
- 动态广播(需要在AndroidManifest中注册,即使应用未启动,也能接收广播,如开机广播)、静态广播(通过代码的registerReceiver()注册。跟随注册组件的生命周期如Activity销毁的时候需要解注册)
ContentProvider
- 用于实现跨应用的数据共享,是安卓中标准的数据访问机制,例如系统的联系人、短信数据、均通过ContentProvider暴露给其他应用
- 通过URI(统一资源标识符)
- 提供query()、insert()、update()、delete()等接口供其他应用操作数据
- 需要AndroidManifest.xml中声明,并配置权限以控制数据访问
进程、应用之间进行通信
应用内事件广播
组件间解耦
解决多个组件间的互斥问题
接收SMS和电话状态
日期和时间变化
- Activity
onCreate
onStart
onResume
onPause
onStop
onRestart
onDestroy
- Fragment
onAttach()
onCreate()
onCreateView()
onActivityCreated()
onStart()
onResume()
onPause()
onStop()
onDestroyView()
onDestroy()
onDeteach()
- 与Activity不同的是
onAttach():当Activity和Fragment建立关联的时候调用
onCreateView(): 当Fragment创建视图的时候调用
onActivityCreated():当Fragment相关联的Activity完成onCreate()之后调用
onDestroyView(): 当Fragment中的布局被移除的时候调用
onDetach(): 当Fragment和Activity解除关联的时候调用
- add()和replace()的区别
add不会重新初始化Fragment,replace每次都会
添加相同的Fragment的时候,replace不会有任何变化,但是add会报异常
replace先remove掉相同id的所有fragment,然后再add当前的这个fragment add是覆盖前一个fragment,所以add一般会伴随hide()和show(),避免布局重叠
用户界面模块化
在多个Activity中复用
适用不同的屏幕尺寸
动态UI更新
与ViewModel结合使用,有效的数据管理。与LiveData结合实现数据的观察和更新
使用FragmentTransaction对Fragment进行增删改、添加、替换、移除
- Intent
组件之间,进行通信和交互,如Activity启动传递数据
启动Service
启动BoardcastReceiver:发送广播
隐式启动
传递多个类型的数据:字符串、整数、序列化对象
Result返回数据
IntentService:异步处理任务
- Service
后台处理:下载文件、播放音乐
位置更新:LocationManager
处理Intent
定期任务:AlarmManager
后台数据同步
与其他组件交互:如Activity、BroadcastReceiver
- Activity相关问题
- 启动模式以及使用场景?
standard:标准模式:如果在mainfest中不设置就默认standard;standard就是新建一个Activity就在栈中新建一个activity实例;
singleTop:栈顶复用模式:与standard相比栈顶复用可以有效减少activity重复创建对资源的消耗,但是这要根据具体情况而定,不能一概而论;
singleTask:栈内单例模式,栈内只有一个activity实例,栈内已存activity实例,在其他activity中start这个activity,Android直接把这个实例上面其他activity实例踢出栈GC掉;
singleInstance :堆内单例:整个手机操作系统里面只有一个实例存在就是内存单例;
taskAffinity
taskAffinity为宿主Activity指定了存放的任务栈[不同于App中其他的Activity的栈],为activity设置taskAffinity属性时不能和包名相同,因为Android团队为taskAffinity默认设置为包名任务栈。
taskAffinity只有和SingleTask启动模式匹配使用时,启动的Activity才会运行在名字和taskAffinity相同的任务栈中。
- 常见应用场景
standard 邮件、mainfest中没有配置就默认标准模式
singleTop 登录页面、WXPayEntryActivity、WXEntryActivity 、推送通知栏
singleTask 程序模块逻辑入口:主页面(Fragment的containerActivity)、WebView页面、扫一扫页面、电商中:购物界面,确认订单界面,付款界面
singleInstanc 系统Launcher、锁屏键、来电显示等系统应用
onSaveInstanceState()和onRestoreInstanceState()使用详解
非正常退出Activity,那么尽管实际 Activity实例已经消失,但是系统还是会记住它已经存在,这样如果用户导航回到它,系统会创建一个新的实例的Activity使用一组保存的数据来描述Activity在被销毁时的状态。系统用于恢复以前状态的已保存数据称为“实例状态”,是存储在Bundle对象中的键值对的集合。
返回键、Finish()不会触发
旋转屏幕会触发
static final String STATE_SCORE = "playerScore";
static final String STATE_LEVEL = "playerLevel";
...
@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
// 保存用户自定义的状态
savedInstanceState.putInt(STATE_SCORE, mCurrentScore);
savedInstanceState.putInt(STATE_LEVEL, mCurrentLevel);
// 调用父类交给系统处理,这样系统能保存视图层次结构状态
super.onSaveInstanceState(savedInstanceState);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); // 记得总是调用父类
// 检查是否正在重新创建一个以前销毁的实例
if (savedInstanceState != null) {
// 从已保存状态恢复成员的值
mCurrentScore = savedInstanceState.getInt(STATE_SCORE);
mCurrentLevel = savedInstanceState.getInt(STATE_LEVEL);
} else {
// 可能初始化一个新实例的默认值的成员
}
...
}
- onConfigurationChanged方法介绍及问题解决
- 当系统的配置信息发生改变时,系统会调用此方法。注意,只有在配置文件 AndroidManifest 中处理了 configChanges属性 对应的设备配置,该方法才会被调用。如果发生设备配置与在配置文件中设置的不一致,则Activity会被销毁并使用新的配置重建。
例如:当屏幕方向发生改变时,Activity会被销毁重建,如果在 AndroidManifest 文件中处理屏幕方向配置信息如下:
则Activity不会被销毁重建,而是调用 onConfigurationChanged 方法。
- Window、Activity、DecorView以及ViewRoot之间的关系
- Activity并不负责视图控制,它只是控制生命周期和处理事件
- 真正控制视图的是Window
- Activity包含了一个Window,Window才是真正代表一个窗口。Activity就像一个控制器,统筹视图的添加与显示,以及通过其他回调方法,来与Window、以及View进行交互
- Window是视图的承载器,内部持有一个 DecorView,而这个DecorView才是 view 的根布局
- Window是一个抽象类,实际在Activity中持有的是其子类PhoneWindow。PhoneWindow中有个内部类DecorView,通过创建DecorView来加载Activity中设置的布局
R.layout.activity_main
。Window 通过WindowManager将DecorView加载其中,并将DecorView交给ViewRoot,进行视图绘制以及其他交互 - DecorView是FrameLayout的子类,它可以被认为是Android视图树的根节点视图
- DecorView作为顶级View,一般情况下它内部包含一个竖直方向的LinearLayout,在这个LinearLayout里面有上下三个部分,上面是个ViewStub,延迟加载的视图(应该是设置ActionBar,根据Theme设置),中间的是标题栏(根据Theme设置,有的布局没有),下面的是内容栏
- ViewRoot可能比较陌生,但是其作用非常重大。所有View的绘制以及事件分发等交互都是通过它来执行或传递的。
- ViewRoot对应ViewRootImpl类,它是连接WindowManagerService和DecorView的纽带,View的三大流程(测量(measure),布局(layout),绘制(draw))均通过ViewRoot来完成。
- ViewRoot并不属于View树的一份子。从源码实现上来看,它既非View的子类,也非View的父类,但是,它实现了ViewParent接口,这让它可以作为View的名义上的父视图。RootView继承了Handler类,可以接收事件并分发,Android的所有触屏事件、按键事件、界面刷新等事件都是通过ViewRoot进行分发的。
- Activity就像个控制器,不负责视图部分。Window像个承载器,装着内部视图。DecorView就是个顶层视图,是所有View的最外层布局。ViewRoot像个连接器,负责沟通,通过硬件的感知来通知视图,进行用户之间的交互
Activity启动过程全解析
-
zygote是什么?有什么作用?
-
zygote意为“受精卵“。Android是基于Linux系统的,而在Linux中,所有的进程都是由init进程直接或者是间接fork出来的,zygote进程也不例外。
-
在Android系统里面,zygote是一个进程的名字。Android是基于Linux System的,当你的手机开机的时候,Linux的内核加载完成之后就会启动一个叫“init“的进程。在Linux System里面,所有的进程都是由init进程fork出来的,我们的zygote进程也不例外。
-
我们都知道,每一个App其实都一个单独的dalvik虚拟机,一个单独的进程。
-
当系统里面的第一个zygote进程运行之后,在这之后再开启App,就相当于开启一个新的进程。而为了实现资源共用和更快的启动速度,Android系统开启新进程的方式,是通过fork第一个zygote进程实现的。所以说,除了第一个zygote进程,其他应用所在的进程都是zygote的子进程
-
SystemServer是什么?有什么作用?它与zygote的关系是什么?
-
SystemServer也是一个进程,而且是由zygote进程fork出来的。
-
这个进程是Android Framework里面两大非常重要的进程之一——另外一个进程就是上面的zygote进程。
-
为什么说SystemServer非常重要呢?因为系统里面重要的服务都是在这个进程里面开启的,比如ActivityManagerService、PackageManagerService、WindowManagerService
-
这些系统怎么开启?
-
在zygote开启的时候,会调用ZygoteInit.main()进行初始化
public static void main(String argv[]) {
...ignore some code...
//在加载首个zygote的时候,会传入初始化参数,使得startSystemServer = true
boolean startSystemServer = false;
for (int i = 1; i < argv.length; i++) {
if ("start-system-server".equals(argv[i])) {
startSystemServer = true;
} else if (argv[i].startsWith(ABI_LIST_ARG)) {
abiList = argv[i].substring(ABI_LIST_ARG.length());
} else if (argv[i].startsWith(SOCKET_NAME_ARG)) {
socketName = argv[i].substring(SOCKET_NAME_ARG.length());
} else {
throw new RuntimeException("Unknown command line argument: " + argv[i]);
}
}
...ignore some code...
//开始fork我们的SystemServer进程
if (startSystemServer) {
startSystemServer(abiList, socketName);
}
...ignore some code...
}
-
ActivityManagerService是什么?什么时候初始化的?有什么作用?
-
ActivityManagerService,简称AMS,服务端对象,负责系统中所有Activity的生命周期
-
为什么说AMS是服务端对象?
-
在Android的框架设计中,使用的也是这一种模式。服务器端指的就是所有App共用的系统服务,比如我们这里提到的ActivityManagerService,和前面提到的PackageManagerService、WindowManagerService等等,这些基础的系统服务是被所有的App公用的,当某个App想实现某个操作的时候,要告诉这些系统服务,比如你想打开一个App,那么我们知道了包名和MainActivity类名之后就可以打开
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_LAUNCHER);
ComponentName cn = new ComponentName(packageName, className);
intent.setComponent(cn);
startActivity(intent);
-
App和AMS(SystemServer进程)还有zygote进程分属于三个独立的进程,他们之间如何通信呢?
-
App与AMS通过Binder进行IPC通信,AMS(SystemServer进程)与zygote通过Socket进行IPC通信。
-
Launcher是什么?什么时候启动的?
-
当我们点击手机桌面上的图标的时候,App就由Launcher开始启动了。但是,你有没有思考过Launcher到底是一个什么东西?
-
Launcher本质上也是一个应用程序,和我们的App一样,也是继承自Activity
public final class Launcher extends Activity
implements View.OnClickListener, OnLongClickListener, LauncherModel.Callbacks,
View.OnTouchListener {
}
-
图标实现了点击、长按等功能
-
Instrumentation是什么?和ActivityThread是什么关系?
-
每个Activity都持有Instrumentation对象的一个引用,但是整个进程只会存在一个Instrumentation对象。当startActivityForResult()调用之后,实际上还是调用了mInstrumentation.execStartActivity()
-
当我们在程序中调用startActivity()的 时候,实际上调用的是Instrumentation的相关的方法。
-
这个类就是完成对Application和Activity初始化和生命周期的工具类
-
包含了callActivityOnCreate()等方法,onCreate()方法就是在这里面调用的
-
Instrumentation类这么重要,为啥我在开发的过程中,没有见到过?
-
Instrumentation这个类很重要,对Activity生命周期方法的调用根本就离不开他,他可以说是一个大管家,但是,这个大管家比较害羞,是一个女的,管内不管外,是老板娘~
-
那么你可能要问了,老板是谁呀?老板是ActivityThread
-
ActivityThread你都没听说过?那你肯定听说过传说中的UI线程吧?是的,这就是UI线程。我们前面说过,App和AMS是通过Binder传递信息的,那么ActivityThread就是专门与AMS的外交工作的。
-
AMS说:“ActivityThread,你给我暂停一个Activity!”ActivityThread就说:“没问题!”然后转身和Instrumentation说:“老婆,AMS让暂停一个Activity,我这里忙着呢,你快去帮我把这事办了把~”于是,Instrumentation就去把事儿搞定了。
-
所以说,AMS是董事会,负责指挥和调度的,ActivityThread是老板,虽然说家里的事自己说了算,但是需要听从AMS的指挥,而Instrumentation则是老板娘,负责家里的大事小事,但是一般不抛头露面,听一家之主ActivityThread的安排。
-
如何理解AMS和ActivityThread之间的Binder通信?
-
Binder本质上只是一种底层通信方式,和具体服务没有关系。为了提供具体服务,Server必须提供一套接口函数以便Client通过远程访问使用各种服务。这时通常采用Proxy设计模式:将接口函数定义在一个抽象类中,Server和Client都会以该抽象类为基类实现所有接口函数,所不同的是Server端是真正的功能实现,而Client端是对这些函数远程调用请求的包装。
-
一个App的程序入口到底是什么?
-
ActivityThread.main()
-
整个App的主线程的消息循环是在哪里创建的?
-
在ActivityThread初始化的时候,就已经创建消息循环了,所以在主线程里面创建Handler不需要指定Looper,而如果在其他线程使用Handler,则需要单独使用Looper.prepare()和Looper.loop()创建消息循环。
- 总结
-
ActivityManagerServices,简称AMS,服务端对象,负责系统中所有Activity的生命周期
-
ActivityThread,App的真正入口。当开启App之后,会调用main()开始运行,开启消息循环队列,这就是传说中的UI线程或者叫主线程。与ActivityManagerServices配合,一起完成Activity的管理工作
-
ApplicationThread,用来实现ActivityManagerService与ActivityThread之间的交互。在ActivityManagerService需要管理相关Application中的Activity的生命周期时,通过ApplicationThread的代理对象与ActivityThread通讯。
-
ApplicationThreadProxy,是ApplicationThread在服务器端的代理,负责和客户端的ApplicationThread通讯。AMS就是通过该代理与ActivityThread进行通信的。
-
Instrumentation,每一个应用程序只有一个Instrumentation对象,每个Activity内都有一个对该对象的引用。Instrumentation可以理解为应用进程的管家,ActivityThread要创建或暂停某个Activity时,都需要通过Instrumentation来进行具体的操作。
-
ActivityStack,Activity在AMS的栈管理,用来记录已经启动的Activity的先后关系,状态信息等。通过ActivityStack决定是否需要启动新的进程。
-
ActivityRecord,ActivityStack的管理对象,每个Activity在AMS对应一个ActivityRecord,来记录Activity的状态以及其他的管理信息。其实就是服务器端的Activity对象的映像。
-
TaskRecord,AMS抽象出来的一个“任务”的概念,是记录ActivityRecord的栈,一个“Task”包含若干个ActivityRecord。AMS用TaskRecord确保Activity启动和退出的顺序。如果你清楚Activity的4种launchMode,那么对这个概念应该不陌生。
-
Service
-
服务是一个后台运行的组件,执行长时间运行且不需要用户交互的任务。即使应用被销毁也依然可以工作
-
开启Service有两种不同的方式:startService和bindService
-
不同的开启方式,Service执行的生命周期方法也不同
-
两种启动方式的区别
-
保活手段
异常捕获
import java.io.File;
import java.io.FileOutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.io.Writer;
import java.lang.Thread.UncaughtExceptionHandler;
import java.lang.reflect.Field;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.os.Build;
import android.os.Environment;
import android.os.Looper;
import android.util.Log;
import android.widget.Toast;
/**
* UncaughtException处理类,当程序发生Uncaught异常的时候,有该类来接管程序,并记录发送错误报告.
*
* @author user
*
*/
public class CrashHandler implements UncaughtExceptionHandler {
public static final String TAG = "CrashHandler";
//系统默认的UncaughtException处理类
private Thread.UncaughtExceptionHandler mDefaultHandler;
//CrashHandler实例
private static CrashHandler INSTANCE = new CrashHandler();
//程序的Context对象
private Context mContext;
//用来存储设备信息和异常信息
private Map<String, String> infos = new HashMap<String, String>();
//用于格式化日期,作为日志文件名的一部分
private DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss");
/** 保证只有一个CrashHandler实例 */
private CrashHandler() {
}
/** 获取CrashHandler实例 ,单例模式 */
public static CrashHandler getInstance() {
return INSTANCE;
}
/**
* 初始化
*
* @param context
*/
public void init(Context context) {
mContext = context;
//获取系统默认的UncaughtException处理器
mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler();
//设置该CrashHandler为程序的默认处理器
Thread.setDefaultUncaughtExceptionHandler(this);
}
/**
* 当UncaughtException发生时会转入该函数来处理
*/
@Override
public void uncaughtException(Thread thread, Throwable ex) {
if (!handleException(ex) && mDefaultHandler != null) {
//如果用户没有处理则让系统默认的异常处理器来处理
mDefaultHandler.uncaughtException(thread, ex);
} else {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
Log.e(TAG, "error : ", e);
}
//退出程序
android.os.Process.killProcess(android.os.Process.myPid());
System.exit(1);
}
}
/**
* 自定义错误处理,收集错误信息 发送错误报告等操作均在此完成.
*
* @param ex
* @return true:如果处理了该异常信息;否则返回false.
*/
private boolean handleException(Throwable ex) {
if (ex == null) {
return false;
}
//使用Toast来显示异常信息
new Thread() {
@Override
public void run() {
Looper.prepare();
Toast.makeText(mContext, "很抱歉,程序出现异常,即将退出.", Toast.LENGTH_LONG).show();
Looper.loop();
}
}.start();
//收集设备参数信息
collectDeviceInfo(mContext);
//保存日志文件
saveCrashInfo2File(ex);
return true;
}
/**
* 收集设备参数信息
* @param ctx
*/
public void collectDeviceInfo(Context ctx) {
try {
PackageManager pm = ctx.getPackageManager();
PackageInfo pi = pm.getPackageInfo(ctx.getPackageName(), PackageManager.GET_ACTIVITIES);
if (pi != null) {
String versionName = pi.versionName == null ? "null" : pi.versionName;
String versionCode = pi.versionCode + "";
infos.put("versionName", versionName);
infos.put("versionCode", versionCode);
}
} catch (NameNotFoundException e) {
Log.e(TAG, "an error occured when collect package info", e);
}
Field[] fields = Build.class.getDeclaredFields();
for (Field field : fields) {
try {
field.setAccessible(true);
infos.put(field.getName(), field.get(null).toString());
Log.d(TAG, field.getName() + " : " + field.get(null));
} catch (Exception e) {
Log.e(TAG, "an error occured when collect crash info", e);
}
}
}
/**
* 保存错误信息到文件中
*
* @param ex
* @return 返回文件名称,便于将文件传送到服务器
*/
private String saveCrashInfo2File(Throwable ex) {
StringBuffer sb = new StringBuffer();
for (Map.Entry<String, String> entry : infos.entrySet()) {
String key = entry.getKey();
String value = entry.getValue();
sb.append(key + "=" + value + "\n");
}
Writer writer = new StringWriter();
PrintWriter printWriter = new PrintWriter(writer);
ex.printStackTrace(printWriter);
Throwable cause = ex.getCause();
while (cause != null) {
cause.printStackTrace(printWriter);
cause = cause.getCause();
}
printWriter.close();
String result = writer.toString();
sb.append(result);
try {
long timestamp = System.currentTimeMillis();
String time = formatter.format(new Date());
String fileName = "crash-" + time + "-" + timestamp + ".log";
if (Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
String path = "/sdcard/crash/";
File dir = new File(path);
if (!dir.exists()) {
dir.mkdirs();
}
FileOutputStream fos = new FileOutputStream(path + fileName);
fos.write(sb.toString().getBytes());
fos.close();
}
return fileName;
} catch (Exception e) {
Log.e(TAG, "an error occured while writing file...", e);
}
return null;
}
}
- Application 中进行init
package com.scott.crash;
import android.app.Application;
public class CrashApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
CrashHandler crashHandler = CrashHandler.getInstance();
crashHandler.init(getApplicationContext());
}
}
视图加载流程
- LayoutInflater
- 用来加载布局
- 根据节点名来创建View对象
- 在createViewFromTag()方法的内部又会去调用createView()
- 使用反射的方式创建出View的实例并返回
- 递归调用rInflate()方法来查找这个View下的子元素,每次递归完成后则将这个View添加到父布局当中
- 把整个布局文件都解析完成后就形成了一个完整的DOM结构,最终会把最顶层的根布局返回,至此inflate()过程全部结束
View绘制流程
-
onMeasure测量
-
measure()方法接收两个参数,widthMeasureSpec和heightMeasureSpec,这两个值分别用于确定视图的宽度和高度的规格和大小
-
MeasureSpec的值由specSize和specMode共同组成的,其中specSize记录的是大小,specMode记录的是规格
-
EXACTLY,大致相当于Match_parent布局,父视图大小确定
-
AT_MOST,大致相当于warp_parent,父视图大小由子视图大小决定
-
一个界面的展示可能会涉及到很多次的measure,因为一个布局中一般都会包含多个子视图,每个视图都需要经历一次measure过程
-
ViewGroup中定义了一个measureChildren()方法来去测量子视图的大小
-
在setMeasuredDimension()方法调用之后,我们才能使用getMeasuredWidth()和getMeasuredHeight()来获取视图测量出的宽高,以此之前调用这两个方法得到的值都会是0
-
onLayout布局
-
onLayout()过程结束后,我们就可以调用getWidth()方法和getHeight()方法来获取视图的宽高
-
getWidth()方法和getMeasureWidth()方法到底有什么区别
-
getMeasureWidth()方法在measure()过程结束后就可以获取到了,而getWidth()方法要在layout()过程结束后才能获取到。另外,getMeasureWidth()方法中的值是通过setMeasuredDimension()方法来进行设置的,而getWidth()方法中的值则是通过视图右边的坐标减去左边的坐标计算出来的
-
onDraw绘制
-
第一步是对视图的背景进行绘制
-
在XML中通过android:background属性设置的图片或颜色。当然你也可以在代码中通过setBackgroundColor()、setBackgroundResource()等方法进行赋值
-
第二步是对视图的内容进行绘制,调用了一下onDraw()方法。空方法,等子类重写
-
对当前视图的所有子视图进行绘制。但如果当前的视图没有子视图,那么也就不需要进行绘制了。因此你会发现View中的dispatchDraw()方法又是一个空方法,而ViewGroup的dispatchDraw()方法中就会有具体的绘制代码
-
绘制的方式主要是借助Canvas这个类,它会作为参数传入到onDraw()方法中,供给每个视图使用。Canvas这个类的用法非常丰富,基本可以把它当成一块画布,在上面绘制任意的东西
-
ViewGroup绘制流程
-
onMeasure测量每个子View的大小
-
使用MeasureChidren测量所有子View,MeasureChild测量单个子View
-
onLayout中对每个子View进行摆放即可
-
视图状态与重绘
-
视图状态
-
enabled 视图是否可用。可以调用setEnable()方法来改变视图的可用状态,传入true表示可用,传入false表示不可用。它们之间最大的区别在于,不可用的视图是无法响应onTouch事件的
-
focused 可以使用requestFocus()这个办法来让视图获得焦点,requestFocus()方法也不能保证一定可以让视图获得焦点,它会有一个布尔值的返回值,如果返回true说明获得焦点成功,返回false说明获得焦点失败。一般只有视图在focusable和focusable in touch mode同时成立的情况下才能成功获取焦点,比如说EditText
-
window_focused表示当前视图是否处于正在交互的窗口中,这个值由系统自动决定,应用程序不能进行改变。
-
selected表示当前视图是否处于选中状态。一个界面当中可以有多个视图处于选中状态,调用setSelected()方法能够改变视图的选中状态,传入true表示选中,传入false表示未选中
-
pressed表示当前视图是否处于按下状态。可以调用setPressed()方法来对这一状态进行改变,传入true表示按下,传入false表示未按下。通常情况下这个状态都是由系统自动赋值的,但开发者也可以自己调用这个方法来进行改变
-
每次View的状态发生了改变,则会触发重绘操作
-
重绘
-
setVisibility()、setEnabled()、setSelected()等方法时都会导致视图重绘,而如果我们想要手动地强制让视图进行重绘,可以调用invalidate()方法来实现。当然了,setVisibility()、setEnabled()、setSelected()等方法的内部其实也是通过调用invalidate()方法来实现的
-
invalidate()方法虽然最终会调用到performTraversals()方法中,但这时measure和layout流程是不会重新执行的,因为视图没有强制重新测量的标志位,而且大小也没有发生过变化,所以这时只有draw流程可以得到执行。而如果你希望视图的绘制流程可以完完整整地重新走一遍,就不能使用invalidate()方法,而应该调用requestLayout()了
Locked Buffer 和 Posted Buffer配合给屏幕输出绘制数据
自定义View
- 自绘控件
- 自绘控件的意思就是,这个View上所展现的内容全部都是我们自己绘制出来的。绘制的代码是写在onDraw()方法中的
- 组合控件
- 组合控件的意思就是,我们并不需要自己去绘制视图上显示的内容,而只是用系统原生的控件就好了,但我们可以将几个系统原生的控件组合到一起,这样创建出的控件就被称为组合控件。
- 继承控件
- 继承控件的意思就是,我们并不需要自己重头去实现一个控件,只需要去继承一个现有的控件,然后在这个控件上增加一些新的功能,就可以形成一个自定义的控件了。这种自定义控件的特点就是不仅能够按照我们的需求加入相应的功能,还可以保留原生控件的所有功能
事件分发机制
事件分发流程?
- 首先事件到达Activity的dispatchTouchEvent()方法
- 接着传递到顶级ViewGroup的dispatchTouchEvent()
- ViewGroup会调用自身的onInterceptTouchEvent来决定是否拦截此事件,返回true事件会交给ViewGroup的onTouchEvent来处理,false则继续向下传递给子视图的dispatchTouchEvent
- 子视图接收到事件以后,会调用自己的dispatchTouchEvent重复上述过程
- 如果事件一直没有被处理,它就会沿着试图层级向上回传
- Touch
- 从手指触摸屏幕到离开屏幕所发生的一系列事件
- 事件分发其实就是将点击事件传递到某个具体的
View
,这个传递的过程就叫做事件分发 - touch事件的层级传递。我们都知道如果给一个控件注册了touch事件,每次点击它的时候都会触发一系列的ACTION_DOWN,ACTION_MOVE,ACTION_UP等事件。这里需要注意,如果你在执行ACTION_DOWN的时候返回了false,后面一系列其它的action就不会再得到执行了。简单的说,就是当dispatchTouchEvent在进行事件分发的时候,只有前一个action返回true,才会触发后一个action
- 所以,基于上面的原则,明明在onTouch事件里返回了false,系统还是在onTouchEvent方法中帮你返回了true。就因为这个原因,才使得前面的例子中ACTION_UP可以得到执行
- 事件分发涉及到的函数以及作用
-
dispatchTouchEvent:是否消耗了本次事件
-
onInterceptTouchEvent:View Group的特有方法:是否拦截了本次事件
-
onTouchEvent:是否处理本次事件
-
伪代码
public boolean dispatchTouchEvent(MotionEvent ev){
boolean consume = false;
if(onInterceptTouchEvent(ev)){
consume = onTouchEvent(ev);
}else{
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
- 对应的根
ViewGroup
,当一个点击事件产生时,Activity
会传递给它,这时它的dispatchTouchEvent
就会被调用,若该ViewGroup
的onInterceptTouchEvent
返回true
,代表拦截该事件,但是否消耗该事件,还要看它的onTouchEvent
的返回值,如果不拦截,则代表将事件分发下去给子View
,接着子View
的dispatchTouchEvent
方法会被调用,如此反复直到事件被最终处理
- 滑动冲突 同方向,不同方向,同方向的滑动冲突1:使用NestscrollView 2:重写onTouchEvent可根据需要请求父布局拦截滑动事件 3:避免嵌套 4:使用CoordinatorLayout 支持更复杂的滚动行为,并智能处理多个不同行为的滚动视图
三方库
Retrofit
JSON 解析库
在项目选型的时候可以使用Google的Gson和阿里巴巴的FastJson两种并行使用,如果只是功能要求,没有性能要求,可以使用google的Gson,如果有性能上面的要求可以使用Gson将bean转换json确保数据的正确,使用FastJson将Json转换Bean
HTTP
-
TCP:三次握手
-
UDP:无握手,IP和端口号直接发送数据即可
-
本质:三次握手 —— 用于“确认通信双方收发数据能力”
-
首先,我让信使运输一份信件给对方,对方收到了,那么他就知道了我的发件能力和他的收件能力是可以的。
-
于是他给我回信,我若收到了,我便知我的发件能力和他的收件能力是可以的,并且他的发件能力和我的收件能力是可以。
-
然而此时他还不知道他的发件能力和我的收件能力到底可不可以,于是我最后回馈一次,他若收到了,他便清楚了他的发件能力和我的收件能力是可以的。
-
缓存策略
-
强制缓存
-
通过设置过期时间来决定是否使用缓存的策略
-
对比缓存
-
通过比对资源上次的修改时间与客户端的请求时间比对,来决定是使用缓存还是重新下发数据
- https的效率一定比http的效率低么?是每一次都低么?
如果使用链接复用,http2协议规则,https只是第一次效率比http低
OKHttp
- 示例用法
//1.新建OKHttpClient客户端
OkHttpClient client = new OkHttpClient();
//新建一个Request对象
Request request = new Request.Builder()
.url(url)
.build();
//2.Response为OKHttp中的响应
Response response = client.newCall(request).execute();
-
onResponse回调的参数是response,一般情况下,比如我们希望获得返回的字符串,可以通过response.body().string()获取;如果希望获得返回的二进制字节数组,则调用response.body().bytes();如果你想拿到返回的inputStream,则调用response.body().byteStream()
-
基本原理
-
内部实现通过一个责任链模式完成,将网络请求的各个阶段封装到各个链条中,实现了各层的解耦。
-
核心代码逻辑
-
通过OkHttpClient的newCall方法创建了一个Call对象,并调用其execute方法;Call代表一个网络请求的接口,实现类只有一个RealCall。execute表示同步发起网络请求,与之对应还有一个enqueue方法,表示发起一个异步请求,因此同时需要传入callback
-
调用Dispatcher的execute
-
记录同步任务、异步任务及等待执行的异步任务;
-
线程池管理异步任务
-
发起/取消网络请求API:execute、enqueue、cancel
-
调用getResponseWithInterceptorChain方法,这也是整个OkHttp实现责任链模式的核心
-
主流程最终走到
chain.proceed(originalRequest)
-
总结起来就是创建下一级责任链,然后取出当前拦截器,调用其intercept方法并传入创建的责任链。++为保证责任链能依次进行下去,必须保证除最后一个拦截器(CallServerInterceptor)外,其他所有拦截器intercept方法内部必须调用一次chain.proceed()方法++,如此一来整个责任链就运行起来了
-
调用Dispatcher的finished方法
-
OkHttp中的并发策略
-
最大并发请求量 maxRequests = 64
-
单个host支持的最大并发量 maxRequestsPerHost = 5
-
同时用三个双端队列存储这些请求:异步任务队列、异步任务等待队列、同步队列
-
为什么使用双端队列?就跟排队一样,后来的排后面,执行任务从头部拿
-
OKHttp缓存策略
-
okHttp的缓存原则是,缓存拦截器会根据请求的信息和缓存的响应的信息来判断是否存在缓存可用,如果有可以使用的缓存,那么就返回该缓存给用户,否则就继续使用责任链模式来从服务器中获取响应。当获取到响应的时候,又会把响应缓存到磁盘上面
-
第一次拿到响应后根据头信息决定是否缓存。下次请求时判断是否存在本地缓存,是否需要使用对比缓存、封装请求头信息等等。如果缓存失效或者需要对比缓存则发出网络请求,否则使用本地缓存。
-
OKHttp的缓存管理
-
两个步骤,一边当我们创建了一个新的连接的时候,我们要把它放进缓存里面;另一边,我们还要来对缓存进行清理。在 ConnectionPool 中,当我们向连接池中缓存一个连接的时候,只要调用双端队列的 add() 方法,将其加入到双端队列即可,而清理连接缓存的操作则交给线程池来定时执行
-
OKHttp拦截器:拦截器的意义 灵活性:通过拦截器,可以自定义请求和响应的处理方式,无需修改整个网络请求逻辑。 代码复用:相同的逻辑可以通过拦截器复用,减少代码重复。 调试与监控:可以方便地记录、分析请求和响应,帮助开发者调试。 增强安全性:可以在请求中添加加密、身份验证等安全措施。 总结来说,OKHttp3 的拦截器让开发者能够更好地控制和管理 HTTP 请求和响应,使得网络操作更加灵活、高效和安全。这种设计使得在处理复杂通信时更具可维护性和可读性。
-
应用拦截器:应用拦截器(Application Interceptors) 作用:可以在请求发送之前和响应收到之后进行操作,比如添加请求头、修改请求等。 使用场景: 添加公共的请求头(如 API 密钥)。 记录日志或监控请求和响应的时间。 提前处理响应数据或错误。
-
网络拦截器:网络拦截器(Network Interceptors) 作用:更底层的拦截器,可以在网络数据传输的过程中处理请求和响应。 使用场景: 监控缓存的使用。 添加或修改请求的缓存控制头。 支持重连、降级等更复杂的网络操作。
-
桥接拦截器
-
缓存拦截器
-
连接拦截器
-
请求拦截器
-
重试拦截器
-
重定向拦截器
Retrofit
-
一个 RESTful 的 HTTP 网络请求框架的封装,本质上网络请求是 OkHttp 完成的,而 Retrofit 仅负责网络请求接口的封装。客户端使用Retrofit ,实际上是使用 Retrofit 接口层封装请求参数、Header、Url 等信息,之后由 OkHttp 完成后续的请求操作,当服务端返回数据之后,OkHttp 再将原始的结果交给 Retrofit,Retrofit然后根据用户的需求,对结果进行解析
-
Retrofit为了实现这种简洁的使用流程,内部使用了优秀的架构设计和大量的设计模式,仔细阅读Retrofit最新版的源码会发现用到大量的设计模式。比如,Retrofit构建过程 会用到建造者模式、工厂方法模式,建网络请求接口实例过程会用到外观模式、代理模式、单例模式、策略模式、装饰模式(建造者模式),生成并执行请求过程 适配器模式(代理模式、装饰模式)。
-
内部主要是用动态代理的方式,动态将网络请求接口的注解解析成HTTP请求,最后执行请求
-
设计模式
-
我们调用接口的方法后是怎么发送请求的?这背后发生了什么?
-
Retrofit 使用了动态代理给我们定义的接口设置了代理,当我们调用接口的方法时,Retrofit 会拦截下来,然后经过一系列处理,比如解析方法的注解等,生成了 Call Request 等OKHttp所需的资源,最后交给 OkHttp 去发送请求, 此间经过 callAdapter,convertr 的处理,最后拿到我们所需要的数据
-
Retrofit 与 OkHttp 是怎么合作的?
-
在Retrofit 中,ServiceMethod 承载了一个 Http 请求的所有参数,OkHttpCall 为 okhttp3.call 的组合包装,由它们俩合作,生成用于 OkHttp所需的 Request以及okhttp3.Call,交给 OkHttp 去发送请求。(在本文环境下具体用的是
call.execute()
)
可以说 Retrofit 为 OkHttp 再封装了一层,并增添了不少功能以及扩展,减少了开发使用成本。
- Retrofit 中的数据究竟是怎么处理的?它是怎么返回 RxJava.Observable 的?
- Retrofit 中的数据其实是交给了 callAdapter 以及 converter 去处理,callAdapter 负责把 okHttpCall 转成我们所需的 Observable类型(本文环境),converter负责把服务器返回的数据转成具体的实体类。
Glide
-
之所以Glide在加载图片的时候要绑定with(context)方法中传入的context的生命周期,如果传入的是Activity,那么在这个Activity销毁的时候Glide会停止图片的加载。这样做的好处在于:避免了消耗多余的资源,也避免了在Activity销毁之后加载图片从而导致空指针问题
-
支持静态图、Gif、本地图加载
-
支持缓存
-
自动压缩图片至合适尺寸
-
源码分析(基本使用)
-
with() Glide调用 ①获取一个RequestManager对象,Glide会根据我们传入with()方法的参数来确定图片加载的生命周期.
②一种是Application类型的参数,另一种是传入非Application类型的参数,第一种直接跟应用程序的生命周期同步,第二种是会向当前的Activity当中添加一个隐藏的Fragment,通过监听Fragment的生命周期来跟Activity生命周期同步,来控制图片加载与否
③在非主线程中使用Glide,都会强制使用Application来处理\ -
load() 以图片Url字符串为例 RequestManager调用
①loadGeneric() ModelLoader对象是用于加载图片的,而我们给load()方法传入不同类型的参数,这里也会得到不同的ModelLoader对象.由于我们刚才传入的参数是String.class,因此最终得到的是StreamStringLoader对象,它是实现了ModelLoader接口的.
②返回一个DrawableTypeRequest对象
③DrawableTypeRequest中的asBitmap()和asGif(),它们分别又创建了一个BitmapTypeRequest和GifTypeRequest,如果没有进行强制指定的话,那默认就是使用DrawableTypeRequest。 -
into() DrawableTypeRequest的父类DrawableRequestBuilder调用 具体实现在DrawableRequestBuilder的父类GenericRequestBuilder中
①构建一个Target对象,用来最终展示图片
根据参数构建不同的Target,如果调用了asBitmap()方法,就会构建出BitmapImageViewTarget对象,否则构建的是GlideDrawableImageViewTarget对象
②buildRequest()方法构建出了一个Request对象,根据之前的load方法中调用的API来组装这个Request对象
③执行这个Request
判断Glide当前是不是处于暂停状态,不是暂停调用Request的begin(),否则添加到待执行队列中
④begin() GenericRequest调用
图片URL地址为空,直接使用占位图
不为空,如果使用了override()为图片指定了一个固定的宽高,一种是没有指定。指定了的话,调用onSizeReady()方法。没指定的话,调用target.getSize()方法。这个target.getSize()方法的内部会根据ImageView的layout_width和layout_height值做一系列的计算,来算出图片应该的宽高,最终都会调用到onSizeReady()方法
⑤onSizeReady() GenericRequest调用
EngineRunnable的run()-->HttpUrlFetcher中的loadData方法具体的请求得到一个InputStream-->之后再一层层封装得到Resource<GlideDrawable>—> GlideDrawableImageViewTarget的setResource设置上图片 -
源码分析(缓存原理)
-
内存缓存
-
内存缓存的主要作用是防止应用重复将图片数据读取到内存当中,而硬盘缓存的主要作用是防止应用重复从网络或其他地方重复下载和读取数据
-
LruCache算法(Least Recently Used),也叫近期最少使用算法。它的主要算法原理就是把最近使用的对象用强引用存储在LinkedHashMap中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除
-
Glide就是使用了这种缓存算法
-
Glide的图片加载过程中会调用两个方法来获取内存缓存,loadFromCache()和loadFromActiveResources()。这两个方法中一个使用的就是LruCache算法,另一个使用的就是弱引用
-
从LruResourceCache中获取到缓存图片之后会将它从缓存中移除,然后在第16行将这个缓存图片存储到activeResources当中。activeResources就是一个弱引用的HashMap,用来缓存正在使用中的图片,我们可以看到,loadFromActiveResources()方法就是从activeResources这个HashMap当中取值的。使用activeResources来缓存正在使用中的图片,可以保护这些图片不会被LruCache算法回收掉
-
取:如果能从内存缓存当中读取到要加载的图片,那么就直接进行回调,如果读取不到的话,才会开启线程执行后面的图片加载逻辑
-
存:首先会将缓存图片从activeResources中移除,然后再将它put到LruResourceCache当中。这样也就实现了正在使用中的图片使用弱引用来进行缓存,不在使用中的图片使用LruCache来进行缓存
-
硬盘缓存
-
和内存缓存类似,硬盘缓存的实现也是使用的LruCache算法,而且Google还提供了一个现成的工具类DiskLruCache
-
分为两种情况,一种是调用decodeFromCache()方法从硬盘缓存当中读取图片,一种是调用decodeFromSource()来读取原始图片。默认情况下Glide会优先从缓存当中读取,只有缓存中不存在要读取的图片时,才会去读取原始图片
-
先去调用DecodeJob的decodeResultFromCache()方法来获取缓存,如果获取不到,会再调用decodeSourceFromCache()方法获取缓存,这两个方法的区别其实就是DiskCacheStrategy.RESULT和DiskCacheStrategy.SOURCE这两个参数的区别
-
Glide的缓存Key是由10个参数共同组成的,包括图片的width、height等等。但如果我们是缓存的原始图片,其实并不需要这么多的参数,因为不用对图片做任何的变化
-
getOriginalKey()方法
-
忽略了绝大部分的参数,只使用了id和signature这两个参数来构成缓存Key。而signature参数绝大多数情况下都是用不到的,因此基本上可以说就是由id(也就是图片url)来决定的Original缓存Key
-
Glide加载图片的时候,假设会使用url地址来组成缓存Key,但是url包含token,token一直在变,会导致缓存失效,怎么办?
-
getCacheKey()方法中的逻辑太直白了,直接就是将图片的url地址进行返回来作为缓存Key的。那么其实我们只需要重写这个getCacheKey()方法,加入一些自己的逻辑判断,就能轻松解决掉这个问题
-
创建一个MyGlideUrl继承自GlideUrl
public class MyGlideUrl extends GlideUrl {
private String mUrl;
public MyGlideUrl(String url) {
super(url);
mUrl = url;
}
@Override
public String getCacheKey() {
return mUrl.replace(findTokenParam(), "");
}
private String findTokenParam() {
String tokenParam = "";
int tokenKeyIndex = mUrl.indexOf("?token=") >= 0 ? mUrl.indexOf("?token=") : mUrl.indexOf("&token=");
if (tokenKeyIndex != -1) {
int nextAndIndex = mUrl.indexOf("&", tokenKeyIndex + 1);
if (nextAndIndex != -1) {
tokenParam = mUrl.substring(tokenKeyIndex + 1, nextAndIndex + 1);
} else {
tokenParam = mUrl.substring(tokenKeyIndex);
}
}
return tokenParam;
}
}
- 在load()方法中传入这个自定义的MyGlideUrl对象,而不能再像之前那样直接传入url字符串了。不然的话Glide在内部还是会使用原始的GlideUrl类,而不是我们自定义的MyGlideUrl类
Glide.with(this)
.load(new MyGlideUrl(url))
.into(imageView);
- Glide的回调与监听
- into()方法还有一个接收Target参数的重载。即使我们传入的参数是ImageView,Glide也会在内部自动构建一个Target对象。而如果我们能够掌握自定义Target技术的话,就可以更加随心所欲地控制Glide的回调了
- 如果我们要进行自定义的话,通常只需要在两种Target的基础上去自定义就可以了,一种是SimpleTarget,一种是ViewTarget
EventBus
- 一种用于Android的事件发布-订阅的事件总线。它简化了应用程序内各个组件之间进行通信的复杂度,尤其是碎片之间进行通信的问题,可以避免由于使用广播通信而带来的诸多不便
- 何为粘性事件?
- 粘性事件是相对普通事件来说的。普通事件是先注册,然后发送事件才能收到;而粘性事件,在发送事件之后再订阅该事件也能收到。并且,粘性事件会保存在内存中,每次进入都会去内存中查找获取最新的粘性事件,除非你手动解除注册
Gradle
- Gradle构建流程
-
编译器将您的源代码转换成 DEX 文件(Dalvik 可执行文件,其中包括在 Android 设备上运行的字节码),并将其他所有内容转换成编译后的资源
-
打包器将 DEX 文件和编译后的资源组合成 APK 或 AAB(具体取决于所选的 build 目标)。 必须先为 APK 或 AAB 签名,然后才能将应用安装到 Android 设备或分发到 Google Play 等商店
-
Build配置文件
-
build.gradle
-
Gradle 设置文件
-
setting.gradle
-
自定义插件
-
Gradle学习路径
-
学习 Groovy(docs.groovy-lang.org/)
-
学习 Gradle DSL(docs.gradle.org/current/jav…
-
学习 Android DSL和Task(google.github.io/android-gra…
-
Gradle脚本的执行时序
-
初始化。分析有哪些module将要被构建,为每个module创建对应的 project实例。这个时候settings.gradle文件会被解析
-
配置:处理所有的模块的 build 脚本,处理依赖,属性等。这个时候每个模块的build.gradle文件会被解析并配置,这个时候会构建整个task的链表(这里的链表仅仅指存在依赖关系的task的集合,不是数据结构的链表)
-
执行:根据task链表来执行某一个特定的task,这个task所依赖的其他task都将会被提前执行
-
Gradle Task
-
日常开发中开发者难免会进行 build/clean project、build apk 等操作
-
实际上这些按钮的底层实现都是通过 Gradle task 来完成的
-
一个 task 就是一个函数,没有什么神秘的地方
-
知道当前 Android 项目中共有哪些 task,可以在项目目录下输入
./gradlew tasks(Mac、Linux)
gradlew tasks(Windows)
- Gradle总结
- 首先解析settings.gradle来获取模块信息,这是初始化阶段;
- 然后配置每个模块,配置的时候并不会执行task;
- 配置完了以后,有一个重要的回调project.afterEvaluate,它表示所有的模块都已经配置完了,可以准备执行task了;
- 执行指定的task。
settings.gradle start
settings.gradle end
> Configure project :
project_build_gradle_config start
project_build_gradle_config end
> Configure project :app
app_build_gradle_config start
app_build_gradle_config end
app evaluate start
app evaluate end
APK包结构
- META-INF:签名文件
- lib/: so库
- res/: 资源文件
- AndroidManifest:名称、版本、权限、组件声明
- assets/: HTML、文本、音频,可以通过AssetManager访问
- classes.dex:编译后的字节码
- resources.arsc: 资源ID映射、字符串资源的ID
打包原理
- 通过AAPT工具进行资源文件(包括AndroidManifest.xml、布局文件、各种xml资源等)的打包,生成R.java文件
- 通过AIDL工具处理AIDL文件,生成相应的Java文件
- 通过Javac工具编译项目源码,生成Class文件
- 通过DX工具将所有的Class文件转换成DEX文件,该过程主要完成Java字节码转换成Dalvik字节码,压缩常量池以及清除冗余信息等工作
- 通过ApkBuilder工具将资源文件、DEX文件打包生成APK文件
- 利用KeyStore对生成的APK文件进行签名
- 如果是正式版的APK,还会利用ZipAlign工具进行对齐处理,对齐的过程就是将APK文件中所有的资源文件举例文件的起始距离都偏移4字节的整数倍,这样通过内存映射访问APK文件的速度会更快
- 编译优化
- Gradle生命周期
- buildSrc先编译,之后是根目录的settings.gradle, 根build.gradle,最后才是module build
- apt是编译中哪个阶段
- APT解析的是java 抽象语法树(AST),属于javac的一部分流程。大概流程:.java -> AST -> .class
- 库引入方式
- CompileOnly 只参与编译,不参与打包
安装流程
- 复制APK到/data/app目录下,解压并扫描安装包。
- 资源管理器解析APK里的资源文件。
- 解析AndroidManifest文件,并在/data/data/目录下创建对应的应用数据目录
- 然后对dex文件进行优化,并保存在dalvik-cache目录下
- 将AndroidManifest文件解析出的四大组件信息注册到PackageManagerService中
- 安装完成后,发送广播
Android 签名原理
- 签名的本质
- 认证:Android 平台上运行的每个应用都必须有开发者的签名。在安装应用时,软件包管理器会验证 APK 是否已经过适当签名,安装程序会拒绝没有获得签名就尝试安装的应用
- 验证完整性:软件包管理器在安装应用前会验证应用摘要,如果破解者修改了 apk 里的内容,那么摘要就不再匹配,验证失败(验证流程见下文方案)
- 签名类型 截止至 Android 11,Android 支持以下三种应用签名方案:
- v1 :基于 Jar 签名;基于 Jar 的签名方案,但存在的问题:完整性覆盖范围不足 & 验证性能差
- v2 :提高验证性能 & 覆盖范围(Android 7.0 Nougat 引入);通过条目内容区、中央目录区之间插入APK 签名分块(APK Signing Block)对v1签名进行了优化
- v3 :支持密钥轮换(Android 9.0 Pie 引入),新增的新块(attr)存储了所有的签名信息,对v2签名进行了优化
- 为了提高兼容性,必须按照 v1、v2、v3 的先后顺序采用签名方案,低版本平台会忽略高版本的签名方案在 APK 中添加的额外数据
- 签名覆盖安装规则
- 对于覆盖安装的情况,签名校验只支持升级而不支持降级。即一个使用 V1 签名的 Apk,可以使用 V2 签名的 Apk 进行覆盖安装,反之则不允许
屏幕适配
-
原因
-
由于Android系统的开放性,任何用户、开发者、OEM厂商、运营商都可以对Android进行定制,修改成他们想要的样子。
-
屏幕碎片化,一万多种屏幕
-
支持Android系统的设备(手机、平板、电视、手表)的增多,设备碎片化、品牌碎片化、系统碎片化、传感器碎片化和屏幕碎片化的程度也在不断地加深
-
概念
-
什么是屏幕尺寸、屏幕分辨率、屏幕像素密度?
-
屏幕尺寸指屏幕的对角线的长度,单位是英寸,1英寸=2.54厘米。比如常见的屏幕尺寸有2.4、2.8、3.5、3.7、4.2、5.0、5.5、6.0等
-
屏幕分辨率是指在横纵向上的像素点数,单位是px,1px=1个像素点。一般以纵向像素横向像素,如19601080
-
屏幕像素密度是指每英寸上的像素点数,单位是dpi,即“dot per inch”的缩写
-
什么是dp、dip、dpi、sp、px?他们之间的关系是什么?
-
px我们应该是比较熟悉的,前面的分辨率就是用的像素为单位,大多数情况下,比如UI设计、Android原生API都会以px作为统一的计量单位,像是获取屏幕宽高等。
-
dip和dp是一个意思,都是Density Independent Pixels的缩写,即密度无关像素,上面我们说过,dpi是屏幕像素密度,假如一英寸里面有160个像素,这个屏幕的像素密度就是160dpi,那么在这种情况下,dp和px如何换算呢?在Android中,规定以160dpi为基准,1dip=1px,如果密度是320dpi,则1dip=2px,以此类推
-
而sp,即scale-independent pixels,与dp类似,但是可以根据文字大小首选项进行放缩,是设置字体大小的御用单位
-
什么是mdpi、hdpi、xdpi、xxdpi?如何计算和区分?
- 方法与解决方案
- 使用Constrainlayout支持各种屏幕尺寸
- 使用wrap_content、match_parent、weight
- 使用相对布局,禁用绝对布局
- 使用LinearLayout的weight属性
- 使用.9图片
- 今日头条屏幕适配
Android缓存机制
-
图片缓存
-
内存缓存
-
LruCache 类
-
磁盘缓存
-
数据库缓存
-
文件缓存
-
LruCache
-
LRU(Least Recently Used)缓存算法
-
核心思想是当缓存满时,会优先淘汰那些近期最少使用的缓存对象
-
采用LRU算法的缓存有两种:LrhCache和DisLruCache,分别用于实现内存缓存和硬盘缓存,其核心思想都是LRU缓存算法
-
LruCache是个泛型类,主要算法原理是把最近使用的对象用强引用(即我们平常使用的对象引用方式)存储在 LinkedHashMap 中。当缓存满时,把最近最少使用的对象从内存中移除,并提供了get和put方法来完成缓存的获取和添加操作
-
核心思想
-
LruCache的核心思想很好理解,就是要维护一个缓存对象列表,其中对象列表的排列方式是按照访问顺序实现的,即一直没访问的对象,将放在队尾,即将被淘汰。而最近访问的对象将放在队头,最后被淘汰。
-
这个队列到底是由谁来维护的,前面已经介绍了是由LinkedHashMap来维护。 而LinkedHashMap是由数组+双向链表的数据结构来实现的。其中双向链表的结构可以实现访问顺序和插入顺序,使得LinkedHashMap中的<key,value>对按照一定顺序排列起来。通过下面构造函数来指定LinkedHashMap中双向链表的结构是访问顺序还是插入顺序
-
源码总结
-
LruCache中维护了一个集合LinkedHashMap,该LinkedHashMap是以访问顺序排序的。当调用put()方法时,就会在结合中添加元素,并调用trimToSize()判断缓存是否已满,如果满了就用LinkedHashMap的迭代器删除队尾元素,即近期最少访问的元素。当调用get()方法访问缓存对象时,就会调用LinkedHashMap的get()方法获得对应集合元素,同时会更新该元素到队头
Android各个版本适配
- Android12
- GPS监听registerGnssStatusCallback
- 蓝牙权限适配
动画
如何优化安卓动画性能
- 使用硬件加速
- 避免在动画中进行复杂的计算
- 避免使用大量的视图层级
- 减少无用重绘和布局
解释“View.invalidate()”和“View.postInvalidate()”的区别
- 一个是立即重绘
- 一个是稍后重绘,子线程重绘
补间动画和帧动画的区别
-
补间动画根据起始和结束状态自动计算中间状态,而帧动画是通过一系列的帧图像实现动画效果。
-
ViewAnimation(视图动画)
-
Canves Animation(画布动画)
-
PropertyAnimation(属性动画)
-
Layout Animation (布局动画)
-
Transition Animation (转场动画:Activity、Fragment切换):活动或片段之间的转换,可以使用 TransitionManager 和 Transition 类来实现。
-
Drawable Animation (帧动画)
-
MotionLayout (ConstrainLayout扩展动画): ML是CL子类,允许在不同的约束间进行平滑过渡,适合复杂场景的复杂动画和布局变化:Lottie 是一个用于解析和播放 JSON 格式动画的库,可以轻松实现复杂的矢量动画,MotionLayout 则是 ConstraintLayout 的扩展,用于构建复杂的视图动画和交互效果。
属性动画和画布动画的区别:属性动画可以动画任何对象的属性,视图动画仅限于对View的表现层进行动画
- Animation和Animator的区别:前者视图相关,后者属性相关
- 如何使用ObjectAnimator创建动画?ObjectAnimator.ofFloat(view, translationX, 0f, 100f)、animator.start()
- 什么是AnimationSet?允许同时管理多个Animator,支持使用play()、with()、before()来设置动画的执行顺序
- Animator.addListener()
- 如何实现减变动画?alpha
- 如何在xml中定义动画?可以使用anim目录中的XML文件定义Animation,并通过AnimationUtil.loadAnimaiton(context, R.anim.anim_file)加载
- ViewPropertyAnimator支持链式调用,如View.animator().translationY(100).setDuration(300).start()
- setInterpolator
- 如何处理屏幕旋转或者其他布局变化对动画的影响?在配置变化中添加适当的保存和恢复状态,以及使用ViewModel来保持动画状态
- 如何优化动画性能?硬件加速、减少重绘
- 长时间动画优化性能:监控状态,适当的时间终止或者重置动画
- RV动画如何做?使用ItemAnimator,重写animateAdd()、animatedRemove()来提供更好的列表条目入场和出场动画
UI适配
- 沉浸式状态栏
- Android手机顶部用于显示各种通知和状态信息的这个栏叫做状态栏
- 通常情况下,我们应用程序的内容都是显示在状态栏下方的。但有时为了实现更好的视觉效果,我们希望将应用程序的内容延伸到状态栏的背后,这种就可以称之为沉浸式状态栏。
- android:fitsSystemWindows
- 为什么时灵时不灵?
- juejin.cn/post/706753…
- 本质代码:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
window.statusBarColor = Color.TRANSPARENT
val frameLayout = findViewById<FrameLayout>(R.id.root_layout)
frameLayout.systemUiVisibility = (SYSTEM_UI_FLAG_LAYOUT_STABLE
or SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
}
}
- 监听状态栏内容并对我们的View进行偏移
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
window.statusBarColor = Color.TRANSPARENT
val frameLayout = findViewById<FrameLayout>(R.id.root_layout)
frameLayout.systemUiVisibility = (SYSTEM_UI_FLAG_LAYOUT_STABLE
or SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
val button = findViewById<Button>(R.id.button)
ViewCompat.setOnApplyWindowInsetsListener(button) { view, insets ->
val params = view.layoutParams as FrameLayout.LayoutParams
params.topMargin = insets.systemWindowInsetTop
insets
}
}
}
即时通讯、音视频、直播
- 视频录制:Camera摄像头
- 屏幕视频录制:MediaProjection
- 编码:MediaCodec、MediaRecoder
- 预览:SurfaceView、GLSurfaceView、TextureView、VideoView
- 即时通讯:文本、语音、视频、实时、高并发、低延迟、可扩展
- 音视频通话和直播的区别:前者点对点,小组,后者一对多
- 安卓即时通讯的常见协议:WebSocket、MQTT、XMPP
- 网络波动处理:监控网络,动态调整编码参数、选择合适的掉线重连策略
- 如何在安卓中使用RTMP实现直播?使用RTMP推流库、如libRTMP、或者第三方SDK、FFmpeg、
- 如何优化音视频的延迟和质量?选择合适的编码格式(H.264)、调整音视频比特率、使用合适的网络协议(UDP)
- 如何保证音视频流的高可靠性?采用FEC前向纠错、重传机制、以及调整网络参数来增强流的稳定性
- 安全与隐私:AES、RSA加密
- 用户体验:输入提示、输入状态、已读、未读、表情、附件支持
- 如何实现消息的离线和同步?本地数据库存储未发送的消息,通过推送服务在用户上线的时候同步聊天记录
- 实现一个简单的即时通讯的思路是什么?用户登录、消息发送接收、保持会话状态、文件传输
Framework
- Android系统对linux kernel、lib库等封装
- 提供WMS、AMS、bind机制、handler-message环境供APP使用
- Activity创建的时候,生成PhoneWindow
- onCreate的setContentView,创建DecorView
- DecorView的addView的时候,加载layout布局
性能优化
- 性能优化的关键在于如何解决存量问题,同时快速发现增量问题
如何设计崩溃恢复机制
- 用SavedState保存关键数据,崩溃时像游戏存盘点,重启后回到最后记录的位置
崩溃定位
崩溃信息收集,记录崩溃发生的进程(前台/后台)
- 线程(是否为UI线程)
- 崩溃堆栈(Java/Native/ANR类型)
- 具体错误类型(如NullPointerException)
- 通过Logcat捕获崩溃堆栈信息
收集机型、系统版本、厂商、CPU架构、设备状态(是否Root)
- 使用adb bugreport生成系统级报告,包含日志、内存状态等等
内存信息
- 分析系统剩余内存、应用内存占用
- 线程数、虚拟内存状态
- 排查内存泄漏、OOM内存不足
- 记录崩溃场景、用户操作路径、以及自定义业务数据等等
崩溃分析工具
- Logcat
- Android Studio Profier
- LeakCanary
- Buigly
- BreakPad
- Crashlytics/Firebase Crashlytics
崩溃原因定位
- Java崩溃分析:空指针异常、资源未释放、线程问题
- Native崩溃分析
- ANR分析
恢复与验证
- 根据崩溃日志中的操作路径,模拟用户行为复现崩溃场景
- 根据堆栈信息修复对应代码
- 优化内存管理
ANR处理、崩溃分析
- ANR定位
- Application Not Responding,程序没有响应
- 出现场景
- 主线程被IO操作(从4.0之后网络IO不允许在主线程中)阻塞。
- 主线程中存在耗时的计算
- 主线程中错误的操作,比如Thread.wait或者Thread.sleep等
- 应用在5秒内未响应用户的输入事件(如按键或者触摸)
- BroadcastReceiver未在10秒内完成相关的处理
- 解决办法
- 基本的思路就是将IO操作在工作线程来处理,减少其他耗时操作和错误操作
- 使用AsyncTask处理耗时IO操作
- 使用Thread或者HandlerThread时,调用Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND)设置优先级,否则仍然会降低程序响应,因为默认Thread的优先级和主线程相同
- 使用Handler处理工作线程结果,而不是使用Thread.wait()或者Thread.sleep()来阻塞主线程。
- Activity的onCreate和onResume回调中尽量避免耗时的代码
- BroadcastReceiver中onReceive代码也要尽量减少耗时,建议使用IntentService处理
- 如果确实有用户感知层面变慢的处理,可以使用进度条
- ANR的Log信息保存在:/data/anr/traces.txt,每一次新的ANR发生,会把之前的ANR信息覆盖掉。
- 从Logcat中可以得到以下信息:
- 导致ANR的包名(com.android.emai),类名(com.android.email.activity.SplitScreenActivity),进程PID(21404)
- 导致ANR的原因:keyDispatchingTimedOut
- 系统中活跃进程的CPU占用率,关键的一句:100%TOTAL: 4.8% user + 7.6% kernel + 87% iowait;表示CPU占用满负荷了,其中绝大数是被iowait即I/O操作占用了。我们就可以大致得出是io操作导致的ANR。
UI优化
- 滑动冲突
场景一:外部滑动方向和内部滑动方向不一致 场景二:外部滑动方向和内部滑动方向一致 场景三:上面两种情况嵌套
-
导致UI卡顿的原因
-
人为在UI线程中做轻微耗时操作,导致UI线程卡顿
-
布局Layout过于复杂,无法在16ms内完成渲染
-
同一时间动画执行的次数过多,导致CPU或GPU负载过重
-
View过度绘制,导致某些像素在同一帧时间内被绘制多次,从而使CPU或GPU负载过重
-
View频繁的触发measure、layout,导致measure、layout累计耗时过多及整个View频繁的重新渲染
-
内存频繁触发GC过多(同一帧中频繁创建内存),导致暂时阻塞渲染操作
-
冗余资源及逻辑等导致加载和执行缓慢
-
ANR
-
布局优化,减少布局层级、布局复用、少用warp_content、避免过度背景绘制
-
开发者模式打开调试GPU过渡绘制
-
GPU呈现模式分析:高于基准线则需要优化
代码优化
- 代码区点击右键->Analyze->Inspect Code–>界面选择你要检测的模块->点击确认开始检测
绘制优化
- 主要是指View的Ondraw方法需要避免执行大量的操作
- ondraw方法不需要创建新的局部对象,这是因为ondraw方法是实时执行的,这样会产品大量的临时对象,导致占用了更多内存,并且使系统不断的GC。降低了执行效率
- Ondraw方法不需要执行耗时操作,在ondraw方法里少使用循环,因为循环会占用CPU的时间。导致绘制不流畅,卡顿等等。Google官方指出,view的绘制帧率稳定在60dps,这要求每帧的绘制时间不超过16ms(1000/60)。虽然很难保证,但我们需要尽可能的降低
线程优化
线程安全的本质是什么?
- 确保在多线程环境下,对共享资源的访问不会导致不一致或错误的状态。通过使用原子操作、锁、条件变量、线程安全的数据结构等多种机制,开发者可以设计出安全的多线程应用程序。在设计时,还需要根据具体的应用场景和需求选择合适的策略,以达到性能和安全性的平衡。
- AsyncTask
- HandlerThread
- ThreadPool
- 并发编程的原则:原子性(synchronized、Lock)、可见性(volatile)、有序性(对于外部线程来说,是固定且可预期的)
- synchronized保证代码层面对于外部线程的有序性
- volatile则能够防止指令重排序
- 线程优雅中断:Thread.interrupt()调用之后,我们需要在耗时操作之前就要判断线程是否已经中断,如!Thread.currentThread().isInterrupted(),捕获到InterruptedException,说明线程被中断了,中断状态会被自动清除。我们可以选择重新设置中断状态或者直接退出循环
- 仅仅依赖线程的interrupt还不能实现优雅的退出,最好是通过标识位比如用volatile变量控制的running,合适的情况下调用stop设置running为false,然后while循环根据running来判断即可优雅终止runnable线程
多线程的本质是什么?协程是真线程还是假线程?
- 协程被称为用户态线程(假线程)其调度由程序控制而非操作系统内核管理
- 协程和线程并非替代关系,而是运行在线程中的轻量级的并发单元
- 本质上是操作系统对CPU计算资源的抽象和调度管理
- 多线程的本质是通过时间片轮转或者并行执行,核心目标是提升资源利用率
并发和并行
- 并发指的是一个人交替执行多个任务
- 并行是多个人同时执行多个任务
- 并发用于解决多个任务在同一时间段内的响应性问题,例如处理用户交互同时进行网络请求。 并行用于提高性能,允许多个任务在同一时刻同时运行,例如在应用中进行多媒体处理或下载多个文件。
同步异步
多个请求同时发,但要全部完成后,再进行下一个任务,用协程来写,怎么写
网络优化
- 时间
- 速度
- 成功率
- 缓存
图片处理
- webp格式
- 缩略图
包体积优化
- 类似于旅行前扔掉多余的行李,只带必需品
- 删除无用资源
- 代码混淆。使用IDE 自带的 proGuard 代码混淆器工具 ,它包括压缩、优化、混淆等功能。
- 使用 Android Lint 删除冗余资源,资源文件最少化等
- 图片压缩处理、矢量图
- Webp
- 避免引入无用或者重复三方库
- 地图使用基础地图
- 网络优化
- 内存泄漏优化
- Crash优化
- 构建优化,为每种设备平台生成必要的APK
- 动态加载
- 常用CPU架构支持,如arm64-v8a、armeabi-v7a、移除其他架构支持
电池优化(Battery Historian)
- 计算优化。算法、for循环优化、Switch..case替代if..else、避开浮点运算
- Android Lint 工具
- 卡顿优化
- 布局优化、绘制优化(移除 XML 中非必须的背景,移除 Window 默认的背景、按需显示占位背景图片)
- 启动速度优化(应用一般都有闪屏页SplashActivity,优化闪屏页的 UI 布局,可以通过 Profile GPU Rendering 检测丢帧情况。)
- 需要进行网络请求时,我们需先判断网络当前的状态
- 在多网络请求的情况下,最好进行批量处理,尽量避免频繁的间隔网络请求
- 在同时有wifi和移动数据的情况下,我们应该直接移动屏蔽数据的网络请求,只有当wifi断开时在调用,因为,wifi请求的耗电量远比移动数据的耗电量低的低
启动优化
- 检查冷启动时间:在logcat中搜索Displayed,此值表示启动过程和完成在屏幕上绘制相应活动之间所经过的时间量优化方案
- 优化闪屏页面
- Application优化,不用在主线程中启动的SDK放在子线程
-
冷启动、暖启动、热启动
-
冷启动:系统还没有这个进程
-
热启动:后台启动
-
冷启动优化
-
减少在Application和第一个Activity的onCreate()方法的工作量; 不要让Application参与业务的操作; 不要在Application进行耗时操作; 不要以静态变量的方式在Application中保存数据; 减少布局的复杂性和深度;
-
冷启动时间:小于1S
-
暖启动
-
相比冷启动,暖启动过程减少了对象初始化、布局加载等工作,启动时间更短。但启动时,系统依然会展示闪屏页,直到第一个 Activity 的内容呈现为止
-
热启动优化
-
热启动时间:小于0.5S
-
用户使用返回键退出应用,然后马上又重新启动应用
-
优化方向
-
主线程中涉及到Shareperference能否在非UI线程执行。
-
Application的创建过程中尽量少的进行耗时操作
-
减少布局的层次,并且生命周期回调的方法中尽量减少耗时的操作
-
崩溃优化
-
崩溃率只是一个数字,我们的出发点应该是让用户有更好的体验
-
Java崩溃
-
Native崩溃
-
Java 崩溃就是在 Java 代码中,出现了未捕获异常,导致程序异常退出。那 Native 崩溃又是怎么产生的呢?一般都是因为在 Native 代码中访问非法地址,也可能是地址对齐出现了问题,或者发生了程序主动 abort,这些都会产生相应的 signal 信号,导致程序异常退出。
-
腾讯的Bugly、阿里的啄木鸟平台、网易云捕、Google 的 Firebase 等等
-
崩溃分析流程
-
进程名、线程名。判断是发生在前台线程还是后台线程,是不是UI线程
-
堆栈、类型。判断是Native崩溃,还是Java崩溃,还是ANR
-
系统信息。记录在文件 /system/etc/event-log-tags中
-
采集维度:机型、系统、厂商、CPU、ABI、Linux 版本等
-
设备状态:是否 root、是否是模拟器
-
OOM、ANR、虚拟内存耗尽等,很多崩溃都跟内存有直接关系
-
系统剩余内存
-
如果是 32 位的 CPU,虚拟内存达到 3GB 就可能会引起内存申请失败的问题
-
Google Play 要求 2019 年 8 月一定要支持 64 位
-
单个进程允许打开的最大文件句柄个数为 1024
-
线程数超过 400 个就比较危险
-
JNI。使用 JNI 时,如果不注意很容易出现引用失效、引用爆表等一些崩溃。我们可以通过 DumpReferenceTables 统计 JNI 的引用表,进一步分析是否出现了 JNI 泄漏等问题
-
崩溃场景。崩溃发生在哪个 Activity 或 Fragment,发生在哪个业务中
-
关键操作路径
-
- 确认严重程度
-
- 查找共性
-
- 尝试复现
-
疑难问题:系统崩溃
-
查找可能的原因
-
尝试规避
-
Hook解决 这里分为 Java Hook 和 Native Hook
内存泄漏检测
-
activityManager.getMemoryClass() 获取内存限制
-
内存分区
共享内存:Zygote进程,跨进程通信内存、多个内存共享
Dalvik/ART运行时:执行字节码、内存管理、垃圾回收
私有内存:其他应用无法访问。堆内存:动态分配对象,栈内存:局部变量、参数
JNI内存:C++代码
系统内存:Android框架、系统服务、UI组件
缓存
- 内存治理
- 三级缓存的海量数据列表是怎么做的?
-
内存泄漏
-
在Activity.onDestroy()被调用之后,view树以及相关的bitmap都应该被垃圾回收
-
什么是内存泄漏呢?内存不在GC的掌控范围之内
-
如何判断一个对象是一个可回收的垃圾对象呢?没错,就是内存过多无法释放的时候,会直接导致OOM。整个项目boom炸了。outofmemory。
-
内存泄漏的原因
非静态内部类(Handler、Thread)
广播使用之后未卸载、单例、文件、数据库使用之后未关闭
频繁大量创建临时变量,导致内存申请速度大于GC回收速度(如ListView的Adapter getView,自定义View的onDraw方法)
资源对象没关闭
Cursor、File、SQLiteCurost
使用Adapter时,没有使用系统缓存的converView
没有即时调用recycle()释放不再使用的bitmap
使用application的context来替代activity相关的context
-
广播注册没取消造成内存泄露
-
Handler应该申明为静态对象, 并在其内部类中保存一个对外部类的弱引用
- 解决方案
使用Android Studio 中的Memory Profier
类似于在房间装了一个摄像头,发现垃圾没有清理就会报警
注册Activity等的生命周期监听,当调用onDestroy的时候判断当前要销毁的对象在内存中的实例对象是否存在,存在的话手动调用一次回收,然后再进行判断,如果还存在就认为是有强引用导致无法回收,被认为是内存泄漏
- 弱引用的写法
private static WeakReference<MainActivity> activityReference;
void setStaticActivity() {
activityReference = new WeakReference<MainActivity>(this);
}
-
另一种方法是在生命周期结束时清除引用,
Activity#onDestory()
方法就很适合把引用置空。 -
开发者必须注意少用非静态内部类,因为非静态内部类持有外部类的隐式引用,容易导致意料之外的泄漏
-
匿名内部类是用于产生后台线程的,这些Java线程是全局的,而且持有创建者的引用(即匿名类的引用),而匿名类又持有外部类的引用。线程是可能长时间运行的,所以一直持有
Activity
的引用导致当销毁时无法回收 -
舍弃简洁偷懒的写法,把子类声明为静态内部类。
-
静态内部类不持有外部类的引用,打破了链式引用
-
新老版写法对比
-
AsyncTask
void startAsyncTask() {
new AsyncTask<Void, Void, Void>() {
@Override protected Void doInBackground(Void... params) {
while(true);
}
}.execute();
}
private static class NimbleTask extends AsyncTask<Void, Void, Void> {
@Override protected Void doInBackground(Void... params) {
while(true);
}
}
void startAsyncTask() {
new NimbleTask().execute();
}
- Handler
void createHandler() {
new Handler() {
@Override public void handleMessage(Message message) {
super.handleMessage(message);
}
}.postDelayed(new Runnable() {
@Override public void run() {
while(true);
}
}, Long.MAX_VALUE >> 1);
}
private static class NimbleHandler extends Handler {
@Override public void handleMessage(Message message) {
super.handleMessage(message);
}
}
private static class NimbleRunnable implements Runnable {
@Override public void run() {
while(true);
}
}
void createHandler() {
new NimbleHandler().postDelayed(new NimbleRunnable(), Long.MAX_VALUE >> 1);
}
- Thread
void scheduleTimer() {
new Timer().schedule(new TimerTask() {
@Override
public void run() {
while(true);
}
}, Long.MAX_VALUE >> 1);
}
private static class NimbleTimerTask extends TimerTask {
@Override public void run() {
while(true);
}
}
void scheduleTimer() {
new Timer().schedule(new NimbleTimerTask(), Long.MAX_VALUE >> 1);
}
- 如果你坚持使用匿名类,只要在生命周期结束时中断线程就可以
private Thread thread;
@Override
public void onDestroy() {
super.onDestroy();
if (thread != null) {
thread.interrupt();
}
}
void spawnThread() {
thread = new Thread() {
@Override public void run() {
while (!isInterrupted()) {
}
}
}
thread.start();
}
-
内存溢出
-
内存溢出就是你要求分配的内存超出了系统能给你的,系统不能满足需求,于是产生溢出
-
内存溢出是指当对象的内存占用已经超出分配内存的空间大小,这时未经处理的异常就会抛出
-
常见的内存溢出情况有:bitmap过大;引用没释放;资源对象没关闭
-
bitmap对象的溢出,显示像素过高或图片尺寸远远大于显示空间的尺寸时,通常都要将其缩放,减小占用内存
-
内存溢出的原因
-
内存泄漏导致
-
由于我们程序的失误,长期保持某些资源(如Context)的引用,垃圾回收器就无法回收它,当然该对象占用的内存就无法被使用,这就造成内存泄露
-
Android 中常见就是Activity被引用,在调用finish之后却没有释放。第二次打开activity又重新创建,这样的内存泄露不断的发生,则会导致内存的溢出。
-
Android的每个应用程序都会使用一个专有的Dalvik虚拟机实例来运行,它是由Zygote服务进程孵化出来的,也就是说每个应用程序都是在属于自己的进程中运行的。Android为不同类型的进程分配了不同的内存使用上限,如果程序在运行过程中出现了内存泄漏的而造成应用进程使用的内存超过了这个上限,则会被系统视为内存泄漏,从而被kill掉,这使得仅仅自己的进程被kill掉,而不会影响其他进程
-
所谓消耗大量的内存的,绝大多数是因为图片加载
-
不存在内存泄漏
-
前台内存占用小于500MB
-
后台内存占用小于500MB
-
后台CPU占用小于2%
-
市面上的手机,主流的运行内存有 LPDDR3、LPDDR4 以及 LPDDR4X。可以看出 LPDDR4 的性能要比 LPDDR3 高出一倍,而 LPDDR4X 相比 LPDDR4 工作电压更低,所以也比 LPDDR4 省电 20%~40%
-
内存造成的第二个问题是卡顿。Java 内存不足会导致频繁 GC
-
具体测试 GC 的性能,例如暂停挂起时间、总耗时、GC 吞吐量,我们可以通过发送 SIGQUIT 信号获得 ANR 日志
-
内存优化步骤
-
先评估内存对应用性能的影响
-
通过崩溃中“异常退出” 和 OOM 的比例进行评估
-
低内存设备更容易出现内存不足引起的异常和卡顿,我们也可以通过查看应用中用户的手机内存在 2GB 以下所占的比例来评估
-
如果我们面向东南亚、非洲用户,那对内存优化的标准就要变得更苛刻一些
-
需要根据设备环境来综合考虑
-
使用类似 device-year-class 的策略对设备分级
-
统一缓存管理,更好地监控每个模块的缓存大小
-
统一进程管理,一个空的进程也会占用10MB内存,减少应用启动的进程数、减少常驻进程、有节操的保活,对低端机内存优化非常重要
-
安装包大小,80MB无法运行在512MB内存的手机上,所以我们要开发一个轻量版本来适配,比如今日头条极速版本就是这个思路。
-
安装包中的代码、图片、资源以及 so 库的大小跟内存都是有关系的,因为这些运行之后,将会被存放在内存的各个存储模块中以方便实用
-
Java 内存泄漏。建立类似 LeakCanary 自动化检测方案
-
OOM 监控。美团有一个 Android 内存泄露自动化链路分析组件Probe
-
Native 内存泄漏监控
-
使用 Androd Profiler 和 MAT 工具
-
触顶率:可以反映 Java 内存的使用情况,如果超过 85% 最大堆限制,GC 会变得更加频繁,容易造成 OOM 和卡顿
-
图片加载优化
-
一个是控制每次加载的数量,第二,保证每次滑动的时候不进行加载,滑动完进行加载。一般情况使用先进后出,而不是先进先出。不过一般我们图片加载都是使用fresco或者Glide等开源库
-
Bitmap优化
-
统一图片库
-
统一图片监控。比如大图监控、重复图片监控、图片总内存大小监控
-
Android Bitmap 内存分配的变化
-
Android 3.0 之前,Bitmap 对象放在 Java 堆,而像素数据是放在 Native 内存中。如果不手动调用 recycle,Bitmap Native 内存的回收完全依赖 finalize 函数回调,熟悉 Java 的同学应该知道,这个时机不太可控
-
Android 3.0~Android 7.0 将 Bitmap 对象和像素数据统一放到 Java 堆中,这样就算我们不调用 recycle,Bitmap 内存也会随着对象一起被回收
-
NativeAllocationRegistry将 Bitmap 内存放到 Native 中,也可以做到和对象一起快速释放,同时 GC 的时候也能考虑这些内存防止被滥用
-
手机内存是否是越大越好?
-
如果一个手机使用的是 4GB 的 LPDDR4X 内存,另外一个使用的是 6GB 的 LPDDR3 内存,那么无疑选择 4GB 的运行内存手机要更加实用一些
-
卡顿优化
-
界面帧率:大于55
-
界面不存在过度绘制
-
原因错综复杂,跟 CPU、内存、磁盘 I/O 都可能有关,跟用户当时的系统环境也有很大关系
-
CPU使用率:小于60%
-
top 命令可以帮助我们查看哪个进程是 CPU 的消耗大户;vmstat 命令可以实时动态监视操作系统的虚拟内存和 CPU 活动;strace 命令可以跟踪某个进程中所有的系统调用
-
CPU饱和度:反映的是线程排队等待 CPU 的情况,也就是 CPU 的负载情况。
-
CPU负载,首先会跟应用的线程数有关,如果启动的线程过多,容易导致系统不断地切换执行的线程,把大量的时间浪费在上下文切换,我们知道每一次 CPU 上下文切换都需要刷新寄存器和计数器,至少需要几十纳秒的时间
-
启动优化
-
弱网络优化
-
缓存控制
-
DNS优化
-
做一个基于OkHttp的网络监控,后期分析
-
传输内容直接使用的
protobuf
格式,所以其不仅仅是网络层上的优化,同时由于流能直接转化成实体类,同时也减少了可序列化的时间。 -
耗电优化
-
渲染优化
-
安装包体积优化
-
国际化
-
空指针判空,并发加锁?仅仅如此就行了么?
安卓安全
代码混淆的原理
- 重命名符号:类命名为a,方法命名为a()等短小无意义的符号
- 删除未使用的代码
- 控制流混淆,使得代码反编译之后,逻辑难于理解
- 字符串加密
- 一般情况下需要混淆的代码:业务逻辑代码(敏感信息或者算法)、第三方库、API调用(支付、账户、加密)、私有类和接口
- 应用程序安全:代码混淆(ProGuard、R8)提高反编译难度、签名、安全库(SafetyNet、Network Security Configuration)
具体步骤
-
启用混淆文件、R8(默认的混淆工具和压缩工具)
-
添加混淆规则,如保留一个特定的Activity
-
保留特定接口和方法
-
字符串保留
-
混淆之后注意充分测试
-
数据安全: AES、RSA、使用权限、SP加密
-
设备安全:设备管理器(远程锁定、擦除)、安全启动、指纹、面部识别
-
网络安全:HTTPS、避免暴露秘钥
-
用户隐私保护:最小权限原则
-
防止逆向:添加反调试代码、模拟器检测
-
安全测试:静态分析、动态分析、渗透测试
-
安全更新:依赖库定期更新
-
应对安全事件:日志记录、应急响应计划
开发效率(自动化打包、持续集成)
- 持续集成,自动化打包
- 跨平台
- React Native
- Flutter
- UNI-APP
插件化
-
插件化的概念就是由宿主APP去加载以及运行插件APP。
-
优点 在一个大的项目里面,为了明确的分工,往往不同的团队负责不同的插件APP,这样分工更加明确。各个模块封装成不同的插件APK,不同模块可以单独编译,提高了开发效率。 解决了上述的方法数超过限制的问题。可以通过上线新的插件来解决线上的BUG,达到“热修复”的效果。 减小了宿主APK的体积
-
缺点
-
无法上架Google商店
Jatpack
- 解决兼容性碎片化 AndroidX代替Support库
- 减少样板代码,封装通用功能(LiveData ViewModel Room)
- 引入MVVM架构设计指南,通过组件强制分离UI逻辑与业务逻辑,提升代码可维护性和可测试性
- 简单应用可引入Live Data、View Model、复杂项目可引入Room、Navigation
- 长期维护项目
- 团队协作项目
Lifecycle
- 管理生命周期的组件
- 每个组件都有
- LifecycleOwner是一个对象,通常是Activity或者Fragment
- getLifecleOwner()方法可以获取该组件的Lifecycle实例
LiveData
Room
ViewModel
为什么能实现配置存活
- 系统用隐藏的Fragment保存ViewModel,就像把玩具暂存到保险箱,旋转屏幕后能直接取回
组件化
如何解决依赖冲突:多个组件依赖同一个库的不同版本的时候
- 强制使用特定版本
configurations.all {
resolutionStrategy {
force 'com.squareup.retrofit2:retrofit:2.9.0'
}
}
- 依赖树分析:./gradlew app:dependencies
- 排除依赖:
implementation ('com.example.mylibrary:library:1.0') {
exclude group: 'com.squareup.retrofit2', module: 'retrofit'
}
- 统一依赖版本
ext {
retrofitVersion = '2.9.0'
}
implementation "com.squareup.retrofit2:retrofit:$retrofitVersion"
动态化方案FLutter、Compose
- 组件间通信
- EventBus:组件间通信,事件传递,通过发布,订阅,提供了一种来实现组件间的解耦和通信,
-
概念解释
-
宿主: 就是当前运行的APP
-
插件: 相对于插件化技术来说,就是要加载运行的apk类文件
-
补丁: 相对于热修复技术来说,就是要加载运行的 .patch, .dex,*.apk等一系列包含dex修复内容的文件。
-
为什么要用组件化?
-
单一工程,业务过度耦合,你中有我,我中有你。牵一发而动全身,每改动任何一个小地方,都需要重新编译整个工程
-
功能测试和系统测试每次都要进行
-
团队协作开发的时候,存在较多的冲突,不得不花时间去沟通和协调。并且在开发的过程中,任何一位成员没办法专注于自己的功能点,影响开发效率
-
不能灵活地对业务进行拆分和组装
-
常见的组件化架构
-
组件化的目标
-
减少编译时间,因为组件化的开发模式,减少了很大的代码量
-
告别结构臃肿
-
各个业务之间相对独立,可独立开发
-
稳定的公共模块采用依赖库方式,提供给各个业务线使用,减少重复开发和维护工作量
-
为新业务随时集成提供了基础,所有业务可上可下,灵活多变
-
降低团队成员熟悉项目的成本,降低项目的维护难度
-
控制代码权限,将代码的权限细分到更小的粒度
组件之间如何通信?
- Intent:Activity与Activity
- Handler和Looper:主线程和工作线程
- Service和Bound Service 后台运行长期执行任务
- Broad CastReceiver 广播
- Content Provider应用之间共享
- Shared Preference 主要用于储存应用设置或者小型数据
- LiveData:Activity与Fragment之间通信
- ViewModel:Activity与Fragment通信,屏幕旋转保持数据
- RxJava
- EventBus
- 用ARouter等路由框架,像快递站统一收发包裹,模块间只认快递单号不直接联系
-
不能像之前的项目一样携带参数直接跳转了
-
通过路由进行跳转
-
开源库的“ActivityRouter” ,有兴趣的同学情直接去ActivityRouter的Github主页学习:ActivityRouter,ActivityRouter支持给Activity定义 URL,这样就可以通过 URL 跳转到Activity,并且支持从浏览器以及 APP 中跳入我们的Activity,而且还支持通过 url 调用方法
-
APT(Annotation Processing Tool)是一种处理注解的工具通过此工具完成路由的标注
-
组件之间如何跳转?
-
业务之间将不再直接引用和依赖,而是通过“路由”这样一个中转站间接产生联系,而Android中的路由实际就是对URL Scheme的封装
-
组件化的实施流程
-
组件模式和集成模式的转换通过apply plugin: ‘com.android.application’和apply plugin: ‘com.android.library’来实现
-
在业务组件的 build.gradle 中指定表单的路径
sourceSets {
main {
if (isModule.toBoolean()) {
manifest.srcFile 'src/main/module/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
}
}
}
- app壳工程有的属性,业务组件都不能有
- 想办法在任何一个业务组件中都能获取到全局的 Context,而且这个 Context 不管是在组件开发模式还是在集成开发模式都是生效的
- library依赖问题:解决重复依赖。Common库统一依赖
- 组件之间的调用和通信工作
- 解决组件之间资源名冲突
架构设计
MVC、MVVM、MVP架构的区别是什么?决定一个项目架构选择什么
- MVC:Controller负责一切:适合传统Web桌面应用
- MVP:Presenter负责逻辑,Controller抽离出一部分职责:增强测试性、分离View依赖、适合高可维护的场景
- MVVM:通过数据绑定实现View与View Model自动同步,View Model管理状态和行为,适合复杂交互场景,但可能因为过度绑定增加复杂度:降低了UI操作难度、数据驱动
- 选型:简单小型项目:MVC
- 高交互、数据绑定需求:MVVM
- 复杂模块化系统:微服务、分层架构
- 电商活动:Flutter、Hybird
MVVM结合Jetpack的实践
- Model-View-ViewModel
- 分离图形用户界面View和业务逻辑Model,通过ViewModel作为中间层实现数据的双向绑定,从而简化界面与数据的交互
- Model:负责存储数据和业务逻辑(网络请求、数据处理)
- View:负责用户界面的展示(HTML、XML、UI控件)
- ViewModel:作为View和Model的桥梁,暴露Model的数据以供View使用;定义View所需的交互逻辑,如按钮点击事件,并更新Model
- 实现数据双向绑定,自动同步View和Model的状态
Clean Architecture
MVI
- MVC
View可以直接访问Model,View里面包含Model信息、业务逻辑
具有一定的分层,model彻底解耦,controller和view并没有解耦
层与层之间的交互尽量使用回调或者去使用消息机制去完成,尽量避免直接持有 controller和view在android中无法做到彻底分离,但在代码逻辑层面一定要分清
业务逻辑被放置在model层,能够更好的复用和修改增加业务
代码示例
-
MVP
-
MVP也是三层,和MVC唯一的差别是Model和View之间不进行通讯,都是通过Presenter完成,Persenter负责逻辑处理
-
MVM
-
MVVM
数据双向绑定,通过数据驱动UI,M提供数据,V提供视图,VM即数据驱动层
-
MVI
-
总结:
-
如果项目简单,没什么复杂性,未来改动也不大的话,那就不要用设计模式或者架构方法,只需要将每个模块封装好,方便调用即可,不要为了使用设计模式或架构方法而使用
-
对于偏向展示型的app,绝大多数业务逻辑都在后端,app主要功能就是展示数据,交互等,建议使用mvvm
-
对于工具类或者需要写很多业务逻辑app,使用mvp或者mvvm都可
-
如果想通过一个项目去学习架构和设计模式,建议用MVC然后在此基础上慢慢挖掘改进。最后你可能发现,改进的最终结果可能就变成了mvp,mvvm
JNI
- Java和C语言之间的桥梁,由于Java是一种半解释语言,可以被反编译,所以涉及安全的代码就使用了C编程
- 再者很多底层功能调用了C语言都实现了,Java没必要重复造轮子,所以就定义了JNI接口的实现,如OpenCV
RxJava
原理
- 通过观察者模式实现事件的生产与消费分离,结合操作符链和线程调度器,提供强大的异步编程能力。其核心是将复杂的异步操作抽象为可链式调用的事件流处理,提高代码可读性和可维护性
-
现实中:警察抓小偷,警察是观察者,小偷是被观察者。警察需要时时刻刻盯紧小偷的一举一动
-
代码中的观察者模式:观察者不需要时时刻刻盯紧被观察者,而是采用注册( Register )或者称为订阅( Subscribe )的方式告诉被观察者,需要在被观察者的状态发生变化的时候通知观察者,这就是代码中的观察者模式
-
RxJava 有四个基本概念
-
Observable
(可观察者,即被观察者)、Observer
(观察者)、subscribe
(订阅)、事件。Observable 和 Observer 通过 subscribe() 方法实现订阅关系,从而 Observable 可以在需要的时候发出事件来通知 Observer。 -
与传统观察者模式不同, RxJava 的事件回调方法除了普通事件
onNext()
(相当于 onClick() / onEvent())之外,还定义了两个特殊的事件:onCompleted()
和onError()
。 -
onCompleted()
: 事件队列完结。RxJava 不仅把每个事件单独处理,还会把它们看做一个队列。RxJava 规定,当不会再有新的onNext()
发出时,需要触发onCompleted()
方法作为标志。 -
onError()
: 事件队列异常。在事件处理过程中出异常时,onError()
会被触发,同时队列自动终止,不允许再有事件发出 -
在一个正确运行的事件序列中,
onCompleted()
和onError()
有且只有一个,并且是事件序列中的最后一个。需要注意的是,onCompleted()
和onError()
二者也是互斥的,即在队列中调用了其中一个,就不应该再调用另一个。并且只要onCompleted()
和onError()
中有一个调用了,都会中止onNext()
的调用。 -
RxJava 的基本实现
-
创建Observer、或者继承了Observer的Subscriver
-
Observer 和 Subscriber 是完全一样的。它们的区别:
-
onStart()
: 这是 Subscriber 增加的方法。它会在 subscribe 刚开始,而事件还未发送之前被调用,可以用于做一些准备工作,例如数据的清零或重置 -
unsubscribe()
: 这是 Subscriber 所实现的另一个接口 Subscription 的方法,用于取消订阅。在这个方法被调用后,Subscriber 将不再接收事件。一般在这个方法调用前,可以使用 isUnsubscribed() 先判断一下状态。unsubscribe()
这个方法很重要,因为在 subscribe() 之后, Observable 会持有 Subscriber 的引用,这个引用如果不能及时被释放,将有内存泄露的风险。所以最好保持一个原则:要在不再使用的时候尽快在合适的地方(例如 onPause() onStop() 等方法中)调用unsubscribe()
来解除引用关系,以避免内存泄露的发生 -
创建Observable
-
Observable 即被观察者,它决定什么时候触发事件以及触发怎样的事件
-
订阅
-
Subscribe
-
事件过程
-
事件流源头(observable)怎么发出数据
-
响应者(subscriber)怎么收到数据
-
怎么对事件流进行操作(operator/transformer)
-
以及整个过程的调度(scheduler)
- 线程调度
-
链式调用
-
RxJava,链式函数的交互模式真的很简洁,终于可以从回调地狱里逃出来了。喜欢的同时不免也会想 RxJava 是如何实现的。这种链式的函数流可以算是建造者模式的一种变形,只不过省去了中间
Builder
而直接返回当前对象来实现 -
RxJava3订阅流程
-
从
Observable#subscribe(Observer)
开始的,而该方法会触发「上游」 Observable 的Observable#subscribeActual(Observer)
方法,而在该「上游」 Observable 中又会触发「上游的上游」Observable 的Observable#subscribeActual(Observer)
方法 -
顶游 Observable 会触发
ObservableEmitter#onNext(T)
方法,在该方法的内部又触发了「下游」 Observer 的onNext(T)
方法,而在该方法内部又会触发「下游的下游」 Observer 的onNext(T)
方法,直至最底层的 Observer —— 我们所自定义的 Observer ——
背压策略
- 生产者和消费者以不同的速度处理数据,当生产者以较快的速度发出数据的时候,消费者可能无法及时处理,于是RxJava提供了背压策略来解决这一问题,以便消费者可以以自己的速度去处理数据而不会被淹没
- Rxjava1不支持,当onNext被调用的时候,消费者可以选择立即处理还是将其排入队列
- RxJava2中则引入了Flowable接受一个Subscriber,允许它以请求的数量方式控制从Flowable接收数据
在 RxJava 中,Flowable 提供了多种内置的背压策略:
- BUFFER
在默认情况下,流会缓冲所有未处理的数据,直到缓冲区已满。此策略可能导致内存消耗上升。
- DROP
当缓冲区已满时,生产者将丢弃最新的数据项(后来的数据会被丢弃)。
- LATEST
当缓冲区已满时,只保留最新的数据项,丢弃之前的项。
- MISSING
生产者和消费者之间没有提供任何背压策略。消费者需要自主管理请求。
- ERROR
当缓冲区已满且限制到达时,抛出 IllegalStateException。这是一种非常严格的策略。
Flowable<Integer> flowable = Flowable.create(emitter -> {
for (int i = 0; i < 100; i++) {
emitter.onNext(i);
}
emitter.onComplete();
}, BackpressureStrategy.BUFFER); // 可选择 DROP, LATEST, MISSING, ERROR
flowable
.observeOn(Schedulers.io())
.subscribeOn(Schedulers.computation())
.subscribe(new FlowableSubscriber<Integer>() {
private Subscription subscription;
@Override
public void onSubscribe(Subscription s) {
this.subscription = s;
subscription.request(1); // 请求项数
}
@Override
public void onNext(Integer item) {
// 处理数据
System.out.println(item);
subscription.request(1); // 请求下一项
}
@Override
public void onError(Throwable t) {
}
@Override
public void onComplete() {
}
});
Dagger2
- 用于依赖注入的框架,减少初始化对象的操作,降低耦合度
Java
String (不可变、字符串池)
集合(种类多、性能、线程安全、实现原理)
多线程(线程安全、线程同步、线程池、实现方式、协程)
泛型(实现设计模式、协变、逆变、类型擦除、运行时泛型)
元编程
hash算法
设计模式
类加载过程
- jvm将.class类文件信息加载到内存并解析成对应的class对象的过程,注意:jvm并不是一开始就把所有的类加载进内存中,只是在第一次遇到某个需要运行的类才会加载,并且只加载一次
String、StringBuilder、StringBuffer
- StringBuffer里面的很多方法添加了synchronized关键字,是可以表征线程安全的,所以多线程情况下使用它。
- 执行速度:StringBuilder > StringBuffer > String
- StringBuilder牺牲了性能来换取速度的,这两个是可以直接在原对象上面进行修改,省去了创建新对象和回收老对象的过程,而String是字符串常量(final)修试,另外两个是字符串变量,常量对象一旦创建就不可以修改,变量是可以进行修改的,所以对于String字符串的操作包含下面三个步骤:
- 创建一个新对象,名字和原来的一样
- 在新对象上面进行修改
- 原对象被垃圾回收掉
GC机制
- 检测垃圾
- 回收垃圾
Java的对象引用
- 强引用
- 通常可以认为是通过new出来的对象,即使内存不足,GC进行垃圾收集的时候也不会主动回收。
Object obj = new Object();
- 弱引用
- 无论内存是否充足,GC进行垃圾收集的时候都会回收。
Object obj = new Object();
WeakReference<Object> weakReference = new WeakReference<>(obj);
- 软引用
- 在内存不足的时候,GC进行垃圾收集的时候会被GC回收。
Object obj = new Object();
SoftReference<Object> softReference = new SoftReference<>(obj);
- 虚引用
- 和弱引用类似,主要区别在于虚引用必须和引用队列一起使用。
Object obj = new Object();
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
PhantomReference<Object> phantomReference = new PhantomReference<>(obj, referenceQueue);
类加载器
-
程序在启动的时候,并不会一次性加载程序所要用的所有class文件,而是根据程序的需要,通过Java的类加载机制(ClassLoader)来动态加载某个class文件到内存当中的,从而只有class文件被载入到了内存之后,才能被其它class所引用。所以ClassLoader就是用来动态加载class文件到内存当中用的。
-
Android中类的加载也是通过ClassLoader来完成,具体来说就是PathClassLoader 和 DexClassLoader 这两个Android专用的类加载器,这两个类的区别如下:
-
PathClassLoader:只能加载已经安装到Android系统中的apk文件(/data/app目录),是Android默认使用的类加载器。
-
DexClassLoader:可以加载任意目录下的dex/jar/apk/zip文件,也就是我们一开始提到的补丁。
-
这两个类都是继承自BaseDexClassLoader,我们可以看一下BaseDexClassLoader的构造函数。
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
这个构造函数只做了一件事,就是通过传递进来的相关参数,初始化了一个DexPathList对象。DexPathList的构造函数,就是将参数中传递进来的程序文件(就是补丁文件)封装成Element对象,并将这些对象添加到一个Element的数组集合dexElements中去。
双亲委派模型
- 当一个类加载器收到加载请求的时候,先向上委托,将请求委派给父类加载器,直至启动类加载器
- 若父类加载器无法加载(未找到对应类),则由当前加载器尝试加载
- 优势:避免类的重复加载
- 保证核心类的安全性(如java.lang.Object始终由启动类加载器加载)
-
自定义ClassLoader、PathClassLoader、BootClassLoader
-
PathClassLoader用于加载应用程序的dex文件
-
BootClassLoader用于加载Framework层的Class文件
-
递归调用,直到加载器或者父加载器返回不为空的结果才会结束递归调用。如果所有的父加载器都加载不到类,那么就会交给当前的类加载器,取调用自己的findClass方法,返回
-
每个ClassLoader实例都有一个父类加载器的引用(不是继承关系,是一个包含的关系),虚拟机内置的类加载器(Bootstrap ClassLoader)本身没有父类加载器,但是可以用做其他ClassLoader实例的父类加载器。
-
为什么使用双亲委派: 1:能够对类划分优先级层次关系 2:避免类的重复加载 3:沙箱安全机制,避免代码被篡改
-
JVM在判断两个class是否相同时,不仅要判断两个类名是否相同,还要判断是否是同一个类加载器加载的。
-
为什么有的场景要打破双亲委派模型机制呢? 1:为了解决某些版本的冲突问题 2:热部署(重新加载已经被加载的类)但是也只能是自定义ClassLoader去加载新的自定义类,也就是有限的热部署
-
所以我们可以自己写一个Object类么? 可以,能通过编译,但是不能被加载,想要被加载就要打破双亲委派模型
集合
-
List
-
Set
-
Map
-
Queue
-
ArrayList和LinkedList的区别是什么? 前者基于动态数组实现,适合随机访问,后者基于双向链表,适合插入删除
-
Set和List区别是什么?Set不允许重复,List可以
-
HashMap和HashTable的区别是什么?前者非线程安全,允许键值对为null,后者线程安全,不允许键值对为null
-
HashMap是如何工作的?将键的哈希值映射到数组的索引,使用链表或者红黑树处理哈希冲突
-
什么是Fail-Fast和Fail-Safe?前者遍历会报异常,后者使用快照的方式遍历,不会异常
-
什么情况下应该使用LinkedHashMap?根据插入顺序有序访问
-
如何从List中去除重复元素?HashSet
-
什么是并发集合?ConcurrentHashMap、CopyOnWriteArrayList
-
ArrayList的扩容机制是什么样?数组大小翻倍,将原数组复制到新数组
- Java集合类主要由两个接口派生出:Collection和Map,这两个接口是Java集合的根接口。
- Collection接口是集合类的根接口,Java中没有提供这个接口的直接的实现类。但是却让其被继承产生了两个接口,就是 Set和List。Set中不能包含重复的元素。List是一个有序的集合,可以包含重复的元素,提供了按索引访问的方式。相当于iOS数组
- Map是Java.util包中的另一个接口,它和Collection接口没有关系,是相互独立的,但是都属于集合类的一部分。Map包含了key-value对。Map不能包含重复的key,但是可以包含相同的value。相当于iOS的字典
泛型
- 泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率。
它提供了编译期的类型安全,确保你只能把正确类型的对象放入 集合中,避免了在运行时出现ClassCastException。
Kotlin
- 更为简洁和已读的语法,减少了样板代码。例如:数据类 (data class):用于持有数据、自动生成equals()、hashCode()、toString方法
- 单行函数字面量:Lambda 表达式和高阶函数使用起来更加简洁,使得函数式编程更加容易。
- 空安全(非空、可变检查、null-safety)可显著减少 nullPointException
- 扩展函数:增强现有类的功能:Kotlin 允许开发者为现有类添加扩展函数,而无需继承或修改类。这使得代码更灵活,功能扩展更简单。
fun String.lastChar(): Char = this[this.length - 1]
- 更好的类型推断:能够根据上下文,自动判断类型
val name = "Kotlin" // 自动推断为 String 类型
- 内置协程支持:Kotlin 的协程(Coroutines)提供了一种简单而强大的方式来处理异步编程。这使得处理并发任务(如网络请求或数据处理)变得更加容易和高效。
// 使用大协程
GlobalScope.launch {
val result = async { longRunningTask() }
println(result.await())
}
- 100%兼容Java
- 库作用域
- 密封类(sealed class)用于限制类的继承,增强类型安全。常用语表示状态或者结果 :Result(),限制继承对象,使得所有可能的类都在一个地方继承
sealed class Result
data class Success(val data: String) : Result()
data class Error(val exception: Exception) : Result()
object Loading : Result()
- 类型安全:编译器可以在编译器检查所有的子类,从而防止意外的状态错误
fun handleResult(result: Result) {
when (result) {
is Success -> println("成功: ${result.data}")
is Error -> println("错误: ${result.exception.message}")
Loading -> println("加载中")
// 不需要 else 分支,这里会提示编译错误,如果有遗漏的子类未处理
}
}
- 强制泛型检查
- 在JVM平台运行字节码
- inline、尾递归:inline 是 Kotlin 中的一个关键字,用于创建内联函数。 内联函数的主要作用是消除函数调用的开销,尤其是在高阶函数(函数作为参数)中使用时。当我们使用内联函数时,编译器会在调用的地方直接插入函数的代码,这样可以提高性能,特别是对于小而频繁调用的函数。
- inline 用于提高函数调用的性能,特别是在高阶函数中,它可以让你避免函数调用的开销。想象一下,把工具放在手边,让朋友更快地修理机器,而不是每次都去拿工具。另一方面,尾递归是一种递归方式,它让函数在调用自身时非常安全,避免了栈溢出的问题。就好比在旅行时只记住当前位置,而不是返回所有之前的位置,这样你就能更安全、更高效地继续前进。
- 高阶函数(接收函数作为参数或者返回函数的函数):例如,Kotlin 中的 with 和 apply 等函数可以看作是高阶函数,它们简化了对象构建和链式调用的过程。
- Kotlin 设计高阶函数的意义在于:增强代码的灵活性和可复用性。支持函数式编程,使代码简洁、易读。简化事件处理、异步任务和回调的编写。提高代码的可读性和可维护性。通过高阶函数,开发者能够以更加灵活和优雅的方式来操作函数和数据,减少代码冗余,提高开发效率。
- 单例类、类代理:在 Java 中,单例通常是通过私有构造函数和静态方法来实现的,需要手动管理线程安全和延迟加载等问题。而 Kotlin 通过 object 关键字自动处理这些细节,使得实现更加简洁和安全。
object Database {
init {
// 初始化数据库
}
fun queryData() {
// 查询数据
}
}
- Kotlin 的单例类设计旨在减少样板代码(boilerplate code),简化单例的实现过程,降低出错的概率,使得访问共享资源更加方便。
- 作用域Api、集合Api:作用域 API 是一组可以在特定对象的上下文中执行代码块函数的函数,主要包括以下几种:
let
apply
run
with
also
功能 let:在当前上下文中执行一个代码块,并返回最后的结果,常用于处理非空对象。
apply:用于初始化对象,返回对象本身,常用于配置对象的属性。
run:在对象的上下文中执行代码块,同时返回最后的结果。
with:用于访问一个对象的多个属性或方法,而不需要重复引用对象。
also:跟 let 类似,但返回的是对象本身,常用于添加额外的操作。
- 函数式编程(Lambda表达式)
- 声明式编程(DSL:领域特定语言,是的代码更加流程容易阅读)
- 可变参数(使用vararg)
- 懒加载 by lazy
- 异常处理 try-catch
- 旨在提高代码可读性和流畅性,减少样板代码(boilerplate code)。通过这些函数,开发者可以在一个对象的上下文中执行多个操作,而不必重复写对象名称,从而使代码更简洁明了。
- Android纯Java项目集成Kotlin
- 在项目的build.gradle中添加kotlin代码库
buildscript {
ext.kotlin_version = '1.5.0'
repositories {
google()
mavenCentral()
maven { url 'https://jitpack.io' }
//友盟
maven { url 'https://repo1.maven.org/maven2/' }
// 设置仓库
maven { url "https://chaquo.com/maven" }
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.2.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
协程
launch和async的区别是什么?为什么要这么设计?意义是什么?
- 前者表示执行某个过程,但不关心结果
- 后者则有一个结果返回,它的使用场景时需要计算并希望得到一个值,使用async表示需要计算并得到一个值
- 使用async表示多个计算可以并行执行,从而提高性能
- 通俗解释就是launch只管发任务,不用担心结果什么时候到
- 但是async则可以await,实现并行,直到拿到所有结果
- 经典题目,ABC三个接口,耗时不同,同时请求,全部结果返回之后,再处理
import kotlinx.coroutines.*
fun main() = runBlocking {
// 使用一个新的 CoroutineScope
val results = CoroutineScope(Dispatchers.IO).async {
// 发起异步请求
val deferredA = async { requestA() }
val deferredB = async { requestB() }
val deferredC = async { requestC() }
// 等待所有请求完成并获取结果
val resultA = deferredA.await()
val resultB = deferredB.await()
val resultC = deferredC.await()
// 返回所有结果
Triple(resultA, resultB, resultC)
}.await() // 获取结果
// 结果拿到后更新UI
updateUI(results.first, results.second, results.third)
}
// 模拟的接口请求
suspend fun requestA(): String {
delay(1000) // 模拟网络延迟
return "Result A"
}
suspend fun requestB(): String {
delay(2000) // 模拟网络延迟
return "Result B"
}
suspend fun requestC(): String {
delay(3000) // 模拟网络延迟
return "Result C"
}
// 更新UI的函数
fun updateUI(resultA: String, resultB: String, resultC: String) {
println("Updating UI with results: $resultA, $resultB, $resultC")
}
如何避免线程阻塞
- 使用挂起函数不让线程卡住,像排队的时候先让后面的人先处理,等轮到再继续
- 轻量级线程,用于处理异步编程,使用launch或者async创建协程
- 协程的基本原理
- 协程干的事就是把异步回调代码拍扁了,捋直了,让异步回调代码同步化。除此之外,没有任何特别之处
- 通过挂起和恢复让状态机状态流转实现把层层嵌套的回调代码变成像同步代码那样直观、简洁
- 它不是什么线程框架,也不是什么高深的内核态,用户态。它其实对于咱们安卓来说,就是一个关于回调函数的语法糖
协程的生命周期
- CoroutineScope(需要手动管理生命周期)
- LifecycleScope(自动管理,跟随组件生命周期)
- GloableScope(全局可用的作用域)
- ViewModelScope (跟随View Model的Scope)
class MainActivity : AppCompatActivity() {
private val job = Job()
private val scope = CoroutineScope(Dispatchers.Main + job)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// 启动协程
scope.launch {
// 在主线程中,但在此处可以助手手动调度代码
val data = withContext(Dispatchers.IO) {
// 这里在 IO 线程中执行网络请求
fetchDataFromNetwork() // 模拟网络请求
}
// 返回到主线程,更新 UI
updateUI(data)
}
}
// 模拟网络请求函数
private suspend fun fetchDataFromNetwork(): String {
delay(2000) // 模拟耗时操作
return "Network Data"
}
private fun updateUI(data: String) {
// 这行代码在主线程,安全更新 UI
println("Data received: $data") // 实际上会更新 UI 元素
}
override fun onDestroy() {
super.onDestroy()
job.cancel() // 取消协程
}
}
编译错误集合
so包错误
- 2 files found with path 'lib/arm64-v8a/libc++_shared.so' from inputs
- 此错误指的是编译过程中,发现多个包里边都存在这个so库
- 解决办法就是在app下的build.gradle下添加如下代码,意思就是如果出现多个so,只选用第一个就行了
packagingOptions{
pickFirst 'lib/arm64-v8a/libc++_shared.so'
pickFirst 'lib/x86_64/libc++_shared.so'
pickFirst 'lib/x86/libc++_shared.so'
pickFirst 'lib/armeabi-v7a/libc++_shared.so'
}
阶段分析
- 开发
- Profiler 性能分析工具的使用
- 编译
- 静态分析工具的使用
- CI、
- 自动化测试、AI
- 测试、
- 灰度
- 动态部署
- 发布
Google Play上架问题
代码混淆
-
启用代码混淆
-
编写Proguard规则,在proguard-rules.pro文件中
-
保留特定类和方法
-
保留特定方法
-
不混淆retrofit、okhttp库
-
可以混淆的类:不会被外部引用的内联类、不对外暴露的API、不需要保持和其他库兼容的代码
-
不能混淆的代码:公共API、动态反射调用的API、第三方库、序列化和反序列化的类、重要的UI类
-
马甲包
-
确保每个应用差异化,不同的登录注册页、首页等
-
名称、包名处理
-
图片素材全部更换
-
ip地址、开发者账号完全隔离
-
本地化处理
-
合理的推广策略