admin管理员组文章数量:1569919
文章目录
- 系统架构
- 应用层
- 应用框架层
- 系统运行库层
- 硬件抽象层(HAL)
- Linux内核层
- 补充
- 通信方式
- Binder
- IPC原理
- Binder原理
- Socket
- handler
- 主线程中
- 子线程中
- Android类加载器
- Service
- 类型
- 前台服务
- 后台服务
- 绑定服务
- 与服务有关的常用方法及作用(涉及Context类和Service类)
- 生命周期
- 手动调用Context#startService()开启服务
- 手动调用Context#stopService()关闭服务
- 手动调用Context#bindService()
- 手动调Context#unbindService()
- 手动调用Context#startService()开启服务后调用Context#bindService()绑定服务
- onStartCommand() 返回值 int
- START_STICKY
- START_NOT_STICKY
- START_REDELIVER_INTENT
- START_STICKY_COMPATIBILITY
- IntentService
- 概念
- 与Service作比较
- Activity
- Activity启动之前的一些事情
- 1.init进程是什么?
- 2.Zygote进程是什么?
- 3.为什么是Zygote来孵化进程,而不是新建进程呢?
- 启动Activity的流程
- Context类相关类关系图
- Activity生命周期
- 注意事项
- 常见周期方法
- onNewIntent
- onRestoreInstanceState(Bundle savedInstanceState)和onSaveInstanceState(Bundle outState)
- 调用时机
- 调用过程
- 注意事项
- 横竖屏切换
- 默认情况下横竖屏切换
- 配置android:configChanges="orientation",防止横竖屏切换时重新创建Activity
- 锁屏和解锁
- 从活动界面A跳转到活动界面B
- 摁下Home键,非长摁
- 启动模式
- Standard
- SingleTop
- SingleTask
- SingleInstance
- Intent标记
- FLAG_ACTIVITY_NEW_TASK
- FLAG_ACTIVITY_SINGLE_TOP
- FLAG_ACTIVITY_CLEAR_TOP
- FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
- 说明一下会出现重置的情况
- FLAG_ACTIVITY_BROUGHT_TO_FRONT
- FLAG_ACTIVITY_REORDER_TO_FRONT
- 亲和性(taskAffinity)
- ViewGroup
- 零碎知识点
- View
- 常用Api
- 坐标系
- requestLayout
- invalidate
- postInvalidate
- View的绘制流程
- ViewRoot和DecorView
- onMeasure
- MeasureSpec
- MeasureSpec.UNSPECIFIED是不是真的不常见?
- onLayout
- onDraw
- Canvas
- Canvas坐标系与绘图坐标系
- Canvas常用Api
- 关于Canvas的save和restore
- drawtext
- Path
- Path常用Api
- moveTo
- lineTo
- quadTo
- cubicTo
- arcTo
- addRoundRect
- close
- reset
- Paint
- 初始化
- 初始化相关flag
- Paint常用Api
- Paint#setTextAlign
- Paint#setShader
- Paint#setPathEffect
- Paint#setTypeface
- 获取文字宽度
- View的事件分发
- onTouch和onTouchEvent
- 自定义View
- 自定义属性
- 滑动冲突的处理
- SurfaceView
- 系统原理
- apk包建立过程(build process)
- 资源冲突
- 安装流程
- 加载图片过程
- proguard和R8
- 混淆
- 混淆jar包
- 网络请求
- 网络库
- OkHttp
- 图片
- 图片加载库
- Android-Universal-Image-Loader
- Volley
- Picasso
- Glide
- 优点
- v4配置
- 注意事项
- Fresco
- 优点
- 缺点
- 图片选择器
- 数据解析
- json
- xml
- 屏幕适配
- 常见的适配方案
- 性能优化
- 奔溃优化
- 内存优化
- 卡顿优化
- 启动优化
- I/O优化
- 存储优化
- 网络优化
- LRU 的原理
- 耗电优化
- UI优化
- 包体积优化
- 热门技术
- 组件化
- 注意事项
- 插件化&&热修复
- 插件化
- 注意事项
- 热修复
- 多渠道打包
- 跨端应用
- 1.Hybird
- 产生背景
- 简介
- 分类
- Cordova
- 2.React Native
- 3.Flutter
- 4.Hippy
- 利于高效开发的一些插件和库记录
- adb
- 配置adb环境变量
- 常用命令
- adb
- adb devices
- adb install -r -t xx.apk
- 卸载apk包
- adb pull
- adb push
- adb shell
- adb shell wm size
- adb help
- adb logcat -s <标签名>
- adb logcat -s TAG:*
- adb shell am start -n 包名/包名+类名(-n 类名,-a action,-d date,-m MIME-TYPE,-c category,-e 扩展数据,等)。
- adb get-product
- adb get-serialno
- adb wifi使用
- adb 连接常用的电脑端模拟器
- DI
- 数据库
- EventBus
- databinding
- gradle
- jetpack
- jni
- rxjava
- 反编译
- 测试
- 架构、设计模式、框架
- 对比
- Android中常用的架构模式
- MVC
- MVP
- MVVM
- 参考链接
- 笔者的感悟
系统架构
Android系统架构分5层。
应用层
系统内置的应用程序以及非系统级的应用程序都是属于应用层。
常见的如 拨号器、邮件、日历、相机等
应用框架层
应用框架层为开发人员提供了开发应用程序所需要的API。这一层是用java编写的,所以也叫Java Framework。
各种管理器以及内容提供者和视图系统。
系统运行库层
分2部分,C/C++程序库和Android运行时库
硬件抽象层(HAL)
硬件抽象层是位于操作系统内核与硬件电路之间的接口层,其目的在于将硬件抽象化。从软硬件测试的角度来看,软硬件的测试工作都可分别基于硬件抽象层来完成,使得软硬件测试工作的并行进行成为可能。
- 开发者不必关心不同硬件设备的差异,只需要按照HAL提供的标准接口访问硬件就可以了。
- 硬件厂商也不需要担心知识产权问题,HAL层帮助硬件厂商隐藏了设备相关模块的核心细节。 HAL层位于用户空间,不属于linux内核,和android源码一样遵循的是Apache license协议
Linux内核层
Android 的核心系统服务基于Linux内核,在此基础上添加了部分Android专用的驱动。系统的安全性、内存管理、进程管理、网络协议栈和驱动模型等都依赖于该内核。
补充
Syscall && JNI:Native与Kernel之间有一层系统调用(SysCall)层;Java层与Native(C/C++)层之间有一层纽带JNI。
系统启动架构图:
通信方式
Binder
Binder作为Android系统提供的一种IPC机制。
IPC原理
从进程角度来看IPC机制
Client进程向Server进程通信,恰恰是利用进程间可共享的内核内存空间来完成底层通信工作的,Client端与Server端进程往往采用ioctl等方法跟内核空间的驱动进行交互。
此外,内核空间的大小是可以通过参数配置调整的。
Binder原理
Binder通信采用C/S架构,从组件视角来说,包含Client、Server、ServiceManager以及binder驱动,其中ServiceManager用于管理系统中的各种服务。架构图如下:
注意事项
- 此处的Service Manager是指Native层的ServiceManager(C++),并非指framework层的ServiceManager(Java)。
- ServiceManager是整个Binder通信机制的大管家,是Android进程间通信机制Binder的守护进程。
- Binder IPC机制,就是指在进程间传输数据(binder_transaction_data),一次数据的传输,称为事务(binder_transaction)。对于多个不同进程向同一个进程发送事务时,这个同一个进程或线程的事务需要串行执行,在Binder驱动中为binder_proc和binder_thread都有todo队列。
- 特别提一下内存映射。Binder IPC机制中涉及到的内存映射通过mmap()来实现,mmap()是操作系统中一种内存映射的方法。内存映射简单的讲就是将用户空间的一块内存区域映射到内核空间。映射关系建立后,用户对这块内存区域的修改可以直接反应到内核空间;反之内核空间对这段区域的修改也能直接反应到用户空间。内存映射能减少数据拷贝次数,实现用户空间和内核空间的高效互动。
一次完整的 Binder IPC 通信过程通常是这样:
- 首先 Binder 驱动在内核空间创建一个数据接收缓存区;
- 接着在内核空间开辟一块内核缓存区,建立内核缓存区和内核中数据接收缓存区之间的映射关系,以及内核中数据接收缓存区和接收进程用户空间地址的映射关系;
- 发送方进程通过系统调用copyfromuser()将数据copy到内核中的内核缓存区,由于内核缓存区和接收进程的用户空间存在内存映射,因此也就相当于把数据发送到了接收进程的用户空间,这样便完成了一次进程间的通信。
想进一步了解的可以查看Gityuan大佬系列文章:Binder系列开篇
Socket
Socket通信方式也是C/S架构,比Binder简单很多。更多的用于Android framework层与native层之间的通信。
举个例子:App进程的创建就是system_server进程通过Socket发送创建进程请求给Zygote进程,然后Zygote进程fork出来的。
handler
handler用于同进程的线程间通信。
Handler,Message,looper和MessageQueue构成了安卓的消息机制,handler创建后可以通过sendMessage将消息加入消息队列,然后looper不断的将消息从MessageQueue中取出来,回调到Hander的handleMessage方法,从而实现线程的通信。
主线程中
主线程里不需要手动开启轮询,Activity在构造过程中已经对Looper进行了初始化并且建立了消息循环。在ActivityThread的main方法中创建了一个当前主线程的looper,开启了消息队列。消息队列是一个无限循环。
looper的无限循环为什么不会ANR?
安卓是由事件驱动的,Looper.loop不断的接收处理事件,每一个点击触摸或者Activity每一个生命周期都是在Looper.loop的控制之下的,looper.loop一旦结束,应用程序的生命周期也就结束了。
思考一下发送ANR情况:事件没有得到处理;事件正在处理,但是没有及时完成。 对事件进行处理的就是looper,所以只能说事件的处理如果阻塞会导致ANR,looper的无限循环不会导致ANR。
子线程中
子线程中新建的Handler对象,需要手动初始化Looper。
需要手动调用looper.prepare(),并通过looper.loop()开启消息轮询(循环)。
主线程Looper从消息队列读取消息,当读完所有消息时,主线程阻塞。子线程往消息队列发送消息,并且往管道文件写数据,主线程即被唤醒,从管道文件读取数据,主线程被唤醒只是为了读取消息,当消息读取完毕,再次睡眠。因此loop的循环并不会对CPU性能有过多的消耗。
class HandlerThread extends Thread{
@Override
public void run() {
//开始建立消息循环
Looper.prepare();//初始化Looper
handler = new Handler(){//默认绑定本线程的Looper,也可以自己指定
@Override
public void handleMessage(Message msg) {
switch(msg.what){
case 0:
Toast.makeText(MainActivity.this, "子线程收到消息", Toast.LENGTH_SHORT).show();
}
}
};
Looper.loop();//启动消息循环
}
}
Android类加载器
不管是插件化还是组件化,都是基于系统的ClassLoader来设计的。只不过Android平台上虚拟机运行的是Dex字节码,一种对class文件优化的产物,传统Class文件是一个Java源码文件会生成一个.class文件,而Android是把所有Class文件进行合并,优化,然后生成一个最终的class.dex,目的是把不同class文件重复的东西只需保留一份,如果我们的Android应用不进行分dex处理,最后一个应用的apk只会有一个dex文件。
Android中的ClassLoader类型也可分为系统ClassLoader和自定义ClassLoader。其中系统ClassLoader包括3种分别是:
- BootClassLoader,Android系统启动时会使用BootClassLoader来预加载常用类,与Java中的Bootstrap ClassLoader不同的是,它并不是由C/C++代码实现,而是由Java实现的。BootClassLoader是ClassLoader的一个内部类。
- PathClassLoader,全名是dalvik/system.PathClassLoader,可以加载已经安装的Apk,也就是/data/app/package 下的apk文件,也可以加载/vendor/lib, /system/lib下的nativeLibrary。
- DexClassLoader,全名是dalvik/system.DexClassLoader,可以加载一个未安装的apk文件。
有兴趣的可以去SDK里看看源码,示例
SDK\platforms\android-29\android.jar!\dalvik\system\BaseDexClassLoader.class
Service
服务是可以在后台执行长时间运行的操作的应用程序组件。 它不提供用户界面。 一旦启动,即使用户切换到另一个应用程序,服务也可能会继续运行一段时间。 此外,组件可以绑定到服务以与其进行交互,甚至可以执行进程间通信(IPC)。 例如,一项服务可以从后台处理网络事务,播放音乐,执行文件I / O或与内容提供者进行交互。
服务在其托管过程的主线程中运行; 除非另行指定,否则该服务不会创建自己的线程,也不会在单独的进程中运行。
可以在配置文件Androidmanifest.xml中设置Service所在线程。
//前缀“:”意思是将名称附加到程序包的标准进程名称中。 remote可以改成任意自定义名字。
android:process=":remote"
类型
前台服务
必须显示通知。
当您的应用程序需要执行用户注意到的任务时才应使用前台服务,即使他们没有直接与应用程序进行交互。因此,前台服务必须显示优先级为PRIORITY_LOW或更高的状态栏通知,这有助于确保用户了解您的应用程序在做什么。
示例代码
//关键方法是Service#startForeground(int id, Notification notification)
Intent notificationIntent = new Intent(this, ExampleActivity.class);
PendingIntent pendingIntent =
PendingIntent.getActivity(this, 0, notificationIntent, 0);
Notification notification =
new Notification.Builder(this, CHANNEL_DEFAULT_IMPORTANCE)
.setContentTitle(getText(R.string.notification_title))
.setContentText(getText(R.string.notification_message))
.setSmallIcon(R.drawable.icon)
.setContentIntent(pendingIntent)
.setTicker(getText(R.string.ticker_text))
.build();
//The integer ID that you give to startForeground() must not be 0.
startForeground(1001, notification);
注意:使用的时候,在 onStartCommand 里面调用 startForeground() 方法把 Service 提升为前台进程级别,不要忘记 onDestroy 里面调用 stopForeground () 方法。
后台服务
一般情况下都是后台服务,后台服务执行用户为直接注意到的操作。
绑定服务
绑定的服务提供了一个客户端-服务器接口,该接口允许组件与该服务进行交互,发送请求,接收结果,甚至通过进程间通信(IPC)跨进程进行交互。 多个组件可以一次绑定到服务,但是当所有组件取消绑定时,该服务将被破坏。
您可以在清单文件中将服务声明为私有服务,并阻止对其他应用程序的访问。
与服务有关的常用方法及作用(涉及Context类和Service类)
手动调用方法 | 作用 |
---|---|
startService() | 启动服务 |
stopService() | 关闭服务 |
bindService() | 绑定服务 |
unbindService() | 解绑服务 |
内部自动调用的方法 | 作用 |
---|---|
onCreat() | 创建服务 |
onStartCommand() | 开始服务 |
onDestroy() | 销毁服务 |
onBind() | 绑定服务 |
onUnbind() | 解绑服务 |
生命周期
手动调用Context#startService()开启服务
多次调用startService(),Service#onCreate()只会回调一次,Service#onStartCommand()会回调多次。
手动调用Context#stopService()关闭服务
正常关闭服务,会回调Service#onDestroy()。
如果是绑定状态的服务,调用stopService()是无效的。此外关闭服务还可以调用Service#stopSelf()。
手动调用Context#bindService()
正常绑定服务,会回调方法Service#onCreate()、Service#onBind()。
手动调Context#unbindService()
正常解绑服务,会先判断是否有调用过Service#onStartCommand(),决定回调Service#onUnbind()后,是否要调用Service#onDestroy()。
手动调用Context#startService()开启服务后调用Context#bindService()绑定服务
onStartCommand() 返回值 int
START_STICKY
常数值:1(0x00000001)。表示Service运行的进程被Android系统强制杀掉之后,Android系统会将该Service依然设置为started状态(即运行状态),但是不再保存onStartCommand方法传入的intent对象,然后Android系统会尝试再次重新创建该Service,并执行onStartCommand回调方法,但是onStartCommand回调方法的Intent参数为null,也就是onStartCommand方法虽然会执行但是获取不到intent信息。如果你的Service可以在任意时刻运行或结束都没什么问题,而且不需要intent信息,那么就可以在onStartCommand方法中返回START_STICKY。
对于在任意时间段内将明确启动和停止运行的事物(例如执行背景音乐播放的服务),此模式有意义。
START_NOT_STICKY
值为2。(0x00000002)。表示当Service运行的进程被Android系统强制杀掉之后,不会重新创建该Service,当然如果在其被杀掉之后一段时间又调用了startService,那么该Service又将被实例化。
如果我们某个Service执行的工作被中断几次无关紧要或者对Android内存紧张的情况下需要被杀掉且不会立即重新创建这种行为也可接受,那么我们便可将onStartCommand的返回值设置为START_NOT_STICKY。比较好的例子就是,轮询对于来自服务器的数据,用一个定时器,指定每过5分钟,启动Service去获取服务器数据(数据不能太多,Service超时时间是20s),假设Service在从服务器获取最新数据的过程中被Android系统强制杀掉,Service不会再重新创建,这都不是事,再过5分钟定时器又启动Service了。
START_REDELIVER_INTENT
值为3。(0x00000003)表示Service运行的进程被Android系统强制杀掉之后,与返回START_STICKY的情况类似,Android系统会将再次重新创建该Service,并执行onStartCommand回调方法,但是不同的是,Android系统会再次将Service在被杀掉之前最后一次传入onStartCommand方法中的Intent再次保留下来并再次传入到重新创建后的Service的onStartCommand方法中,这样我们就能读取到intent参数。
适用于Service需要依赖具体的Intent才能运行(需要从Intent中读取相关数据信息等),并且在强制销毁后有必要重新创建运行的情况。
START_STICKY_COMPATIBILITY
值为0。(0x00000000)
从onStartCommand(Intent,int,int)返回的常量:START_STICKY的兼容版本,它不保证onStartCommand(Intent,int,int)被杀死后将再次被调用。
IntentService
概念
intentService是Service的子类,继承service,拥有service的全部生命周期,包含了service的全部特性。
//部分IntentService源码,基于sdk28
private volatile Looper mServiceLooper;
private volatile ServiceHandler mServiceHandler;
private final class ServiceHandler extends Handler {
public ServiceHandler(Looper looper) {
super(looper);
}
@Override
public void handleMessage(Message msg) {
onHandleIntent((Intent)msg.obj);
stopSelf(msg.arg1);
}
}
@Override
public void onCreate() {
super.onCreate();
HandlerThread thread = new HandlerThread("IntentService[" + mName + "]");
thread.start();
mServiceLooper = thread.getLooper();
mServiceHandler = new ServiceHandler(mServiceLooper);
}
public IntentService(String name) {
super();
mName = name;
}
@Override
public int onStartCommand(@Nullable Intent intent, int flags, int startId) {
onStart(intent, startId);
return mRedelivery ? START_REDELIVER_INTENT : START_NOT_STICKY;
}
@Override
public void onStart(@Nullable Intent intent, int startId) {
Message msg = mServiceHandler.obtainMessage();
msg.arg1 = startId;
msg.obj = intent;
mServiceHandler.sendMessage(msg);
}
分析:
- 内部有一个加了volatile关键字的Looper对象和Handler对象;
- 在onCreate方法里创建了一个默认的工作线程;
- HandlerThread里的run方法里调用了Looper#prepare()(研究过handler机制的人都知道,prepare方法里新建的Looper对象);
- 构造方法带了一个参数,用来命名工作线程的。
与Service作比较
- IntentService直接创建了一个工作线程。而Service在其托管过程的主线程中运行, 除非另行指定,否则Service不会创建自己的线程,也不会在单独的进程中运行。
- 可以多次启动IntentService,每一个耗时操作都会以工作队列的形式在IntentService的onHandleIntent回调中执行,并且每次执行一个工作线程。Service也可以启动多次,但是如果是耗时操作,建议手动开线程去执行。
- IntentService请求完成后自己会调用stopSelf(),Service需要手动关闭。
- IntentService不需要关注onBind,默认返回null;同时IntentService只需要关心onHandleIntent方法,甚至不需重写Service最关心的onStartCommand方法或者onStart方法。
Activity
最开始只有一块运行着原始 Android 系统的板砖。
Surface Flinger 的出现是为了更加方便地完成 UI 渲染。
Window 的出现是为了管理 UI 内容的排版。
Window 觉得负担太重将责任下发到 View 身上。
View 通过组合模式,在递归的帮助下蹭蹭蹭地完成排版工作。
Activity 的出现是为了满足多窗口管理和傻瓜式视图管理的需要。
Activity启动之前的一些事情
- init进程:init是所有linux程序的起点,是Zygote的父进程。解析init.rc孵化出Zygote进程。
- Zygote进程:Zygote是所有Java进程的父进程,所有的App进程都是由Zygote进程fork生成的。
- SystemServer进程:SystemServer是Zygote孵化的第一个进程。SystemServer负责启动和管理整个Java framework,包含AMS,PMS等服务。
- Launcher:Zygote进程孵化的第一个App进程是Launcher。
1.init进程是什么?
Android是基于linux系统的,手机开机之后,linux内核进行加载。加载完成之后会启动init进程。
init进程会启动ServiceManager,孵化一些守护进程,并解析init.rc孵化Zygote进程。
2.Zygote进程是什么?
所有的App进程都是由Zygote进程fork生成的,包括SystemServer进程。Zygote初始化后,会注册一个等待接受消息的socket,OS层会采用socket进行IPC通信。
3.为什么是Zygote来孵化进程,而不是新建进程呢?
每个应用程序都是运行在各自的Dalvik虚拟机中,应用程序每次运行都要重新初始化和启动虚拟机,这个过程会耗费很长时间。Zygote会把已经运行的虚拟机的代码和内存信息共享,起到一个预加载资源和类的作用,从而缩短启动时间。
启动Activity的流程
抛开热启动的情况,比如应用在后台时点击app图标。这时候直接走走Activity生命周期就行。下面我们讨论冷启动的情况。
从点击屏幕上的app图标,到进入响应app,背后经历了Activity和AMS(ActivityManagerService)的反反复复的通信过程。在AndroidManifest.xml文件中定义默认启动的activity,设置activity的action和category属性标签。Launcher为每个app的icon图标提供了启动这个app时所需要的Intent信息。这些信息在app安装时由PackageManagerService从app的AndroidManifest.xml文件中读取。
- 点击app图标,Launcher进程使用Binder IPC向system__server进程发起startActivity请求;
- system__server进程收到1中的请求后,向zygote进程发送创建新进程的请求;
- zygote进程fork出新的App进程
- App进程通过Binder IPC向system__server进程发起attachApplication请求;
- system__server进程收到4中的请求后,通过Binder IPC向App进程发送scheduleLauncherActivity请求;
- App进程的ApplicationThread线程收到5的请求后,通过handler向主线程发送LAUNCHER_ACTIVITY消息;
- 主线程收到6中发送来的Message后,反射创建目标Activity,回调oncreate等方法。一系列方法执行完后进行UI渲染,渲染结束之后便可以看到App界面了。
下图适用于api26之前,因为api26(8.0)后android framework代码发生了变化,部分类被废弃和移除了。
比如在26sdk里ActivityManagerProxy被移除了,方式改变了
sdk版本为28,截取部分Instrumentation类的exeStartActivity方法,ActivityManager.getService()实现使用的aidl。
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, String target,
Intent intent, int requestCode, Bundle options) {
//省略...
int result = ActivityManager.getService()
.startActivity(whoThread, who.getBasePackageName(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()),
token, target, requestCode, 0, null, options);
checkStartActivityResult(result, intent);
//省略...
}
而在sdk版本25时候,是使用ActivityManagerNative.getDefault()获取ActivityManagerProxy执行其startActivity方法。26之后简化了不少代码,明显看出使用了aidl,而之前使用的Binder,讲实话AIDL是基于Binder机制实现的,所以其实差不多。
Context类相关类关系图
Activity生命周期
注意事项
由于优先级和资源回收的原因,可能同一个操作在不同场景的生命周期不一样。下文如果未特别说明,默认资源没有被回收。
- Activity第一次启动:onCreate->onStart->onResume
- 跳转新的活动界面或者按Home键切换到桌面:onPause->onStop;
- 当Activity主题为透明主题或者是Dialog时(Theme为Translucent或Dialog)。跳转到这种活动界面时原活动界面只会调onPause(不会调用onStop);原活动界面重新回到前台,直接调onResume(不会调用onReStart和onStart)。
- 用户回到已启动的activity:正常情况下,onStop->onRestart->onStart->onResume;如果高优先级的应用需要内存,Activity被杀死回收再回到该界面:onStop->onCreate->onStart->onResume,不经过onRestart,资源被回收了。
- onRestart调用场景:摁下home键,再切回来;从一个活动跳到另一个活动,摁返回键切回来;从某APP的某个活动跳转另外一个APP的活动,然后切回来。
常见周期方法
-
onCreate:create表示创建,这是Activity生命周期的第一个方法,也是我们在android开发中接触的最多的生命周期方法。此时Activity还在后台,不可见。所以动画不应该在这里初始化。
-
onStart:start表示启动,这是Activity生命周期的第二个方法。此时Activity已经可见了,但是还没出现在前台,我们还看不到,无法与Activity交互。
-
onResume:在前台,和onStart同样可见,不同的是onStart在后台,onResume在前台。
-这个阶段可以打开独占设备。(可与用户交互,我们称为独占设备) -
onPause:正在停止。此时Activity在前台并可见,我们可以进行一些轻量级的存储数据和去初始化的工作。不能做耗时操作,因为在跳转Activity时只有当一个Activity执行完了onPause方法后另一个Activity才会启动,而且android中指定如果onPause在500ms即0.5秒内没有执行完毕的话就会强制关闭Activity。
-
onStop:stop表示停止,此时Activity已经不可见了,但是Activity对象还在内存中,没有被销毁。这个阶段的主要工作也是做一些资源的回收工作。
-
onDestroy:destroy表示毁灭,可以理解为之后Activity被销毁,我们可以将还没释放的资源释放,以及进行一些回收工作。
-
onRestart:restart表示重新开始,Activity在这时可见。这个方法一般不做操作。
onNewIntent
对于在其程序包中将launchMode设置为“singleTop”的活动,或者在客户端调用startActivity(Intent)时使用Intent#FLAG_ACTIVITY_SINGLE_TOP标志的活动,将调用此方法。
不论在哪种情况,当活动在活动堆栈的顶部重新启动对于正在启动的活动的新实例,将使用onNewIntent()调用现有实例,其目的是用于重新启动它。
举例说明
- A 在栈顶,ActivityA模式为singleTop,然后start activitya→activitya start activitya,这时候顺序执行onPause→onNewIntent→onResume。
- A不在栈顶,ActivityA模式为singleTask,不管是ActivityA里启动自己还是从ActivityB启动,都会调用onNewIntent,只是生命周期执行顺序不一样。启动自己和例1一致,从B启动A则是onNewIntent→onRestart→onStart→onResume。
- ActivityA模式为singleInstance,情况和2差不多,只是界面跳转的时候明显感觉到是不同的栈。
总结:只要不是standard模式,其他3个模式下都可能调用onNewIntent方法。无非是启动自己或者从其他界面启动他。
onRestoreInstanceState(Bundle savedInstanceState)和onSaveInstanceState(Bundle outState)
调用时机
Activity的异常情况下(例如转动屏幕或者被系统回收的情况下),会调用到onSaveInstanceState和onRestoreInstanceState。其他情况不会触发这个过程。但是按Home键或者启动新Activity或者锁屏仍然会单独触发onSaveInstanceState的调用。
onSaveInstanceState()的调用遵循一个重要原则,即当系统存在“未经你许可”时销毁了我们的activity的可能时,则onSaveInstanceState()会被系统调用,这是系统的责任,因为它必须要提供一个机会让你保存你的数据。以下情况都会调用,可能是单独触发保存,也可能出现重建,触发onRestoreInstanceState方法。
- 按下HOME键
- 长按HOME键,选择运行其他的程序
- 按下电源按键或者自动锁屏(锁屏)
- 从一个活动跳转另一个活动
- 横屏切竖屏
- 设备语言设定(会引发重建)
- 其他可能引发活动被销毁并且这不是用户所希望的情况
调用过程
旧的Activity要被销毁时,由于是异常情况下的,所以除了正常调用onPause, onStop, onDestroy方法外,还会在调用onStop方法前,调用onSaveInstanceState方法。
新的Activity重建时,我们就可以通过onRestoreInstanceState方法取出之前保存的数据并恢复,onRestoreInstanceState的调用时机在onCreate之后。onRestoreInstanceState()在onStart()和onPostCreate(Bundle)之间调用
注意事项
- 当用户主动去销毁一个Activity时,例如在应用中按返回键,onSaveInstanceState()就不会被调用。因为在这种情况下,用户的行为决定了不需要保存Activity的状态。
- 通常onSaveInstanceState()只适合用于保存一些临时性的状态,而onPause()适合用于数据的持久化保存。
- 当系统调用Activity的的onRestoreInstanceState(Bundle savedInstanceState)时, 同时Bundle的getParcelable方法得到Parcelable对象,然后把该Parcelable对象传给View的onRestoreInstanceState (Parcelable state)。在的View的onRestoreInstanceState中从Parcelable读取保存的数据以便View使用。
横竖屏切换
google在android3.2中添加了screensize改变的通知,在转屏的时候,不仅是orientation发生了改变,screensize同样也发生了改变。都2020年了,目标版本大家都写的挺高的,姑且还行记一下吧。
默认情况下横竖屏切换
简单点说就是销毁了再新建。此外onSaveInstanceState调用时机以实践结果为例,并不代表onSaveInstanceState调用不能在onPause之前。
- 3.2之后
onPause→onSaveInstanceState(Bundle outState)→onStop→onDestroy→onCreate→onStart→onResume
- 3.2之前
onSaveInstanceState(Bundle outState)→onPause→onStop→onDestroy→onCreate→onStart→onResume
配置android:configChanges=“orientation”,防止横竖屏切换时重新创建Activity
- 如果targetSdkVersion<=12,所有版本都有效(都2020了–!)
- 3.2版本开始参数需要多加个screenSize才生效。
<activity android:name=".MainActivity"
android:configChanges="orientation|screenSize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
最后提一哈,网上说的什么横屏执行1次,竖屏执行2次,还有设置参数keyboardHidden。其实都是误导,我实践了发现只有screenSize配合orientation才有用。
锁屏和解锁
//应用活动没有在锁屏时被回收,所以调用onRestart
//锁屏
onPause→onSaveInstanceState→onStop
//解锁
onRestart→onStart→onResume
从活动界面A跳转到活动界面B
A:onPause ->onSaveInstanceState-> B:onCreate -> B:onStart -> B:onResume -> A:onStop
摁下Home键,非长摁
onPause→onSaveInstanceState→onStop
启动模式
Standard
无论任务栈中是否已经有这个Activity的实例,系统都会创建一个新的Activity实例。
默认模式,通常会在启动活动时创建一个新的活动实例,尽管这种行为可能会因引入其他选项(例如Intent.FLAG_ACTIVITY_NEW_TASK)而改变。
SingleTop
当一个singleTop模式的Activity已经位于任务栈的栈顶,再去启动它时,不会再创建新的实例,如果不位于栈顶,就会创建新的实例,只有重新启动时候才会调用onNewIntent。
如果在启动活动时,前台中已经存在与用户交互的同一活动类的实例,则请重新使用该实例。 此现有实例将通过新的Intent收到对Activity.onNewIntent()的调用。如果没有,则会正常新建一个并入栈。
SingleTask
如果Activity已经位于栈顶,系统不会创建新的Activity实例,和singleTop模式一样。但Activity已经存在但不位于栈顶时,系统就会把该Activity移到栈顶,并把它上面的activity出栈。这个模式活动如果栈里有实例,则再次调用一定跑onNewIntent。
如果在启动活动时已经有一个以该活动开始的任务正在运行,那么将当前任务置于最前面,而不是启动新实例。现有实例将收到一个Activity.onNewIntent()的调用,该调用具有正在启动的新Intent,并且设置了Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT标志。 这是singleTop模式的超集,在该模式下,如果已经在堆栈的顶部启动了活动的实例,它将接收那里描述的Intent(不设置FLAG_ACTIVITY_BROUGHT_TO_FRONT标志)。
SingleInstance
singleInstance模式也是单例的,但和singleTask不同,singleTask只是任务栈内单例,而 singleInstance修饰的活动整个系统只有一个。启动一个SingleInstance模式的活动时,系统会创建一个新的任务栈,而且这个任务栈里只有一个实例。
还有个细节是,SingleInstance的活动实例化的时候,如果存在,直接调用onNewIntent。
关于这种模式需要注意,因为在SingleInstance活动里打开其他活动时,都是放在活动应该放的栈里,所以会造成返回并返回不到这个SingleInstance活动的情况,以及出现当前交互界面是某个SingleInstance活动,摁home键回到桌面,如果不是在最近活动里选择,直接点击app应用,会直接回到应用最后启动的活动界面,不会回到SingleInstance活动。
这样设计也有好处,实现了多个应用共享一个应用,使用场景如闹钟、相机等。
Intent标记
- 注意:通过launchMode属性为Activity指定的行为,可被启动Activity的intent所包含的标记替换。
- 注意:有些启动模式可通过清单文件定义,但不能通过 intent 标记定义,同样,有些启动模式可通过 intent 标记定义,却不能在清单中定义。
- 下文如果没有提及配置文件,默认使用standard。
FLAG_ACTIVITY_NEW_TASK
在新任务中启动 Activity。如果您现在启动的 Activity 已经有任务在运行,则系统会将该任务转到前台并恢复其最后的状态,而 Activity 将在 onNewIntent() 中收到新的 intent。
注意事项
- 关注点是在Task。大多数情况是引入到某个taskAffinity对应的栈里。
- 如果目标Activity实例或者Task不存在,则一定会新建Activity,并将目标Task移动到前台。
- 如果Activity存在,却并不一定复用,也不一定可见。(需要结合活动的launchMode和设置的其他标签结合分析)
- 对于非Activity启动的Activity(比如Service中启动的Activity)需要显示的设置Intent.FLAG_ACTIVITY_NEW_TASK。
- singleTask及singleInstance在ActivityManagerService中被预处理后,隐形的设置了Intent.FLAG_ACTIVITY_NEW_TASK.
- standard及singletTop的Activity不会被设置Intent.FLAG_ACTIVITY_NEW_TASK。除非手动设置,通过Intent#setFlags(int flags).
//api29源码ContextImpl.java中的startActivity()有如下判断,印证了建议显示设置标记的原因。
if ((intent.getFlags() & Intent.FLAG_ACTIVITY_NEW_TASK) == 0
&& (targetSdkVersion < Build.VERSION_CODES.N
|| targetSdkVersion >= Build.VERSION_CODES.P)
&& (options == null
|| ActivityOptions.fromBundle(options).getLaunchTaskId() == -1)) {
throw new AndroidRuntimeException(
"Calling startActivity() from outside of an Activity "
+ " context requires the FLAG_ACTIVITY_NEW_TASK flag."
+ " Is this really what you want?");
}
Intent.FLAG_ACTIVITY_NEW_TASK的初衷是在Activity目标taskAffinity的Task中启动
如果不是在Activity中启动的,那就可以看做不是用户主动的行为,也就说这个界面可能出现在任何APP之上,如果不用Intent.FLAG_ACTIVITY_NEW_TASK将其限制在自己的Task中,那用户可能会认为该Activity是当前可见APP的页面,这是不合理的。
我举个例子。比如打开QQ音乐后回到桌面,此时QQ音乐在后台,我再打开新浪微博,这时候QQ音乐发来了一个推送消息告诉我我的会员需要续费了,我点击推送消息进入QQ音乐开通VIP的界面,这时候摁返回键,并不会回到新浪微博,而是QQ音乐的主页。
FLAG_ACTIVITY_SINGLE_TOP
Intent.FLAG_ACTIVITY_SINGLE_TOP多用来做辅助作用,跟launchmode中的singleTop作用一样。如果要启动的 Activity是当前Activity(即位于返回堆栈顶部的Activity),则现有实例会收到对 onNewIntent() 的调用,而不会创建 Activity 的新实例。(onPause->onNewIntent->onResume)
FLAG_ACTIVITY_CLEAR_TOP
这种情况也比较复杂,通常是结合其他标签一起使用。
- 单独使用FLAG_ACTIVITY_CLEAR_TOP并且没有设置特殊的launchmode。
目标是当前Task栈,栈里有实例时,则不会启动该 Activity 的新实例,而是会销毁位于它之上的所有其他 Activity,并通过 onNewIntent() 将此 intent传送给它的已恢复实例(现在位于堆栈顶部)。(调用onNewIntent之后会调用onCreate,可以视为是新的)
- FLAG_ACTIVITY_CLEAR_TOP结合FLAG_ACTIVITY_SINGLE_TOP使用。
同单独使用FLAG_ACTIVITY_CLEAR_TOP差不多,区别在于,不会"NEW",只调用onNewIntent
- FLAG_ACTIVITY_CLEAR_TOP结合FLAG_ACTIVITY_NEW_TASK
因为FLAG_ACTIVITY_NEW_TASK 的存在,所以配置文件里的android:taskAffinity属性能起到作用,正常情况下会新开辟一个任务栈。如果当前任务栈找不到,会去目标Task中去找。
- FLAG_ACTIVITY_CLEAR_TOP结合FLAG_ACTIVITY_NEW_TASK 结合FLAG_ACTIVITY_SINGLE_TOP
同3一样,只不过找到之后清空顶部,直接调用onNewIntent,不会新建。
FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
如果这么设置,并且此活动是在新任务中启动或将现有任务置于顶部,则它将作为任务的前门启动。
仅在需要时候,将将任务重置为初始状态。我们知道任务栈里有很多活动Activity,使用FLAG_ACTIVITY_RESET_TASK_IF_NEEDED常见例子:从桌面打开app。之后摁home键回到桌面,再次点击app图标,会回到app之前停留的页面。没错,这种情况没有重置任务。
说明一下会出现重置的情况
当我们将一个后台的task重新回到前台时,系统会在特定情况下为这个动作附带一个FLAG_ACTIVITY_RESET_TASK_IF_NEEDED标记,意味着必要时重置task,这时FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET就会生效。经过测试发现,对于一个处于后台的应用,如果在launcher中点击应用,这个动作中含有FLAG_ACTIVITY_RESET_TASK_IF_NEEDED标记,长按Home键,然后点击最近记录,这个动作不含FLAG_ACTIVITY_RESET_TASK_IF_NEEDED标记,所以前者会清除(仅清除使用Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET启动的所有活动以及这些活动之上的所有活动),后者不会。
FLAG_ACTIVITY_BROUGHT_TO_FRONT
该标志通常不是由应用程序代码设置的,而是由系统为您设置的,比如上文singleTask就有提到。
FLAG_ACTIVITY_REORDER_TO_FRONT
这个用到的可能性比较大, 会是活动在栈中的位置发生改变。比如说原来栈中情况是A,B,C,D
,在D中启动B(加入该flag),栈中的情况会是A,C,D,B
(并且调用onNewIntent)
亲和性(taskAffinity)
“亲和性”表示 Activity 倾向于属于哪个任务。默认情况下,同一应用中的所有 Activity 彼此具有亲和性。
亲和性可在两种情况下发挥作用:
- 当启动 Activity 的 intent 包含 FLAG_ACTIVITY_NEW_TASK 标记时。
- 当 Activity 的 allowTaskReparenting 属性设为 “true” 时。
ViewGroup
零碎知识点
- ViewGroup的子类必须实现onLayout方法。确定子View的位置。
- ViewGroup不需要实现onDraw方法。如果明确需要画自己的话,需要调用setViewNotDraw(false)。(View必须重载onDraw方法)
- 可以调用drawChild方法重新回调每个子视图的draw方法
boolean draw(Canvas canvas, ViewGroup parent, long drawingTime)
View
常用Api
坐标系
requestLayout
作用:当某些更改使该视图的布局无效时调用此方法。 这将安排视图树的布局遍历。
时机:当视图层次结构当前处于布局阶段({@link#isInLayout()}中时,不应调用此方法。如果正在进行布局,则可以在当前布局阶段结束时接受该请求(然后布局将再次运行) )或绘制当前帧并进行下一个布局之后。
注意事项:只是对View树重新布局layout过程包括measure()和layout()。如果view的l,t,r,b没有必变,那就不会触发onDraw;但是如果这次刷新是在动画里,mDirty非空,就会导致onDraw。
invalidate
- 在ui线程刷新view。
- 层层上传到父级,直到传递到ViewRootImpl后触发了scheduleTraversals(),然后整个View树开始重新按照View绘制流程进行重绘任务。在ui线程刷新view。
postInvalidate
- 在工作线程刷新view。
- 原理是invalidate+handler。
- 最终会调用ViewRootImpl.dispatchInvalidateDelayed()方法。
View的绘制流程
一说到绘制流程,就会想到onMeasure、onLayout、onDraw这三个方法,但是有没想过为什么我们开启一个App或是点开一个Activity就会触发这一系列流程呢?有必要先理一理App启动流程和Activity的启动流程。上文有详细介绍,这里简单回顾下:
ActivityThread的main方法。里面做了一系列操作,比如开启主线程的消息轮询(Looper初始化);再比如通过ActivityManager的getService获取ActivityManagerService的代理,达到跨进程通信目的。
源码不深入研究了,大概知道顺序就行Activity-》Window->View
知识点
- 一个Activity对应一个Window。
- Window的唯一实现类为PhoneWindow。
- DecorView,被PhoneWindow持有着,作为顶级View,他的初始化于setContentView方法。
- ContentView是来装载我们设置的布局文件的ViewGroup。
- 有兴趣的可以研究下源码(android/app/ActivityThread.java)关注几个方法handleLaunchActivity、performLaunchActivity、handleResumeActivity、attach。
- 沿着代码一路找,会有:ActivityThread#handleResumeActivity-》WindowManagerImpl#addView-》ViewRootImpl#scheduleTraversals-》TraversalRunnable执行-》ViewRootImpl#performTraversals-》ViewRootImpl中的performMeasure、performLayout、performDraw。
- 看源码,通知OnResume方法之前只是准备好了Decorview,而真正的绘制(onMeasure、onLayout、onDraw是在OnResume调用之后)这也就是为什么在OnCreate里无法获取View的宽高的原因
ViewRoot和DecorView
- ViewRoot对应ViewRootImpl类,是连接WindowManager和DecorView的纽带。View的三大流程是通过ViewRoot完成的。
- View绘制流程从 performTraversals开始,依次进行测量、定位、绘制。
- 从上往下,绘制过程有点像树的深度遍历。
onMeasure
//视图的实际测量工作执行在这里,被measure方法拉起的
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec)
MeasureSpec
MeasureSpec封装了从父级传递到子级的布局要求。系统把view的LayoutParams 根据 父容器施加的规则(父容器的SpecMode) 转换成 view的MeasureSpec,然后使用这个MeasureSpec确定view的测量宽高。View的MeasureSpec是由LayoutParams和父容器的MeasureSpec共同决定。顶级view,即DecorView,是由窗口尺寸和自身LayoutParams决定。
MeasureSpec即view的测量规格:高2位的SpecMode,低30位的SpecSize。
-
UNPECIFIED父容器对view不限制,要多大给多大,一般系统内部使用。
-
EXACTLY,父容器检测出view所需大小,view最终大小就是SpecSize的值。对应 LayoutParams中的matchParent、具体数值 两种模式。
-
AT_MOST,父容器制定了可用大小即SpecSize,view的大小不能大于这个值,具体要看view的具体实现。对应LayoutParams中的wrap_content。
View类中onMeasure是设置了固定宽高的,看方法
public static int getDefaultSize(int size, int measureSpec) {
int result = size;
int specMode = MeasureSpec.getMode(measureSpec);
int specSize = MeasureSpec.getSize(measureSpec);
switch (specMode) {
case MeasureSpec.UNSPECIFIED:
result = size;
break;
case MeasureSpec.AT_MOST:
case MeasureSpec.EXACTLY:
result = specSize;
break;
}
return result;
}
UNSPECIFIED,一般是系统使用,暂时先不关心他。这里view大小直接取size,就是getSuggestedMinimumWidth()/getSuggestedMinimumHeight(),意思是建议的最小宽高。
MeasureSpec.UNSPECIFIED是不是真的不常见?
在日常定制View时,确实很少会专门针对这个模式去做特殊处理,大多数情况下,都会把它当成MeasureSpec.AT_MOST一样看待,就比如最最常用的TextView,它在测量时也是不会区分UNSPECIFIED和AT_MOST的。
不过,虽说这个模式比较少直接接触到,但很多场景下,我们已经在不知不觉中用上了,比如RecyclerView的Item,如果Item的宽/高是wrap_content且列表可滚动的话,那么Item的宽/高的测量模式就会是UNSPECIFIED。还有就是NestedScrollView和ScrollView,因为它们都是扩展自FrameLayout,所以它们的子View会测量两次,第一次测量时,子View的heightMeasureSpec的模式是写死为UNSPECIFIED的。
我们在自定义ViewGroup过程中,如果允许子View的尺寸比ViewGroup大的话,在测量子View时就可以把Mode指定为UNSPECIFIED。
AT_MOST、EXACTLY,直接取specSize。specSize确定综合父亲控件和当前控件进行分析。
childLayoutParams(纵)/parentSpecMode | EXACTLY | AT_MOST | UNSPECIFIED |
---|---|---|---|
dp/px | EXACTLY childSize | EXACTLY childSize | EXACTLY childSize |
match_parent | EXACTLYparentSize | AT_MOST parentSize | UNSPECIFIED 0 |
wrap_content | AT_MOST parentSize | AT_MOST parentSize | UNSPECIFIED 0 |
值得注意的是,有时候你会发现你自定义View的wrap_content竟然不起作用,还有padding也不起作用,下面我们来分析一波:
- 首先如果是UNSPECIFIED,我们先放过,不理会。
- 如果是AT_MOST,这个时候自定义控件的wrap_content的效果和match_parent一样(parentSize)为什么?简单说:在View类中,当该View的布局宽高值为wrap_content,或match_parent时,该View测量的大小就是MeasureSpec中的测量大小–>SpecSize(具体查看源码)。因此,在自定义View时,需要重写onMeasure(w,h)用来处理wrap_content的情况,然后调用setMeasuredDimession(w,h)完成测量。
- 如果是EXACTLY,其实也不用管太多,默认就挺好。
onLayout
protected void onLayout(boolean changed, int l, int t, int r, int b)
onLayout 则是进行摆放,这一过程比较简单,因为我们从 onMeasure 中已经得到各个子View 的宽高。父View 只要按照自己的逻辑负责给定各个子View 的 左上坐标 和 右下坐标 即可。
onDraw
protected void onDraw(Canvas canvas)
在 canvas 绘制自身需要绘制的内容。
Canvas
Canvas坐标系与绘图坐标系
- Canvas坐标系指的是Canvas本身的坐标系,Canvas坐标系有且只有一个,且是唯一不变的,其坐标原点在View的左上角,从坐标原点向右为x轴的正半轴,从坐标原点向下为y轴的正半轴。
- Canvas的drawXXX方法中传入的各种坐标指的都是绘图坐标系中的坐标,而非Canvas坐标系中的坐标。默认情况下,绘图坐标系与Canvas坐标系完全重合,即初始状况下,绘图坐标系的坐标原点也在View的左上角,从原点向右为x轴正半轴,从原点向下为y轴正半轴。但不同于Canvas坐标系,绘图坐标系并不是一成不变的,可以通过调用Canvas的translate方法平移坐标系,可以通过Canvas的rotate方法旋转坐标系,还可以通过Canvas的scale方法缩放坐标系,而且需要注意的是,translate、rotate、scale的操作都是基于当前绘图坐标系的,而不是基于Canvas坐标系,一旦通过以上方法对坐标系进行了操作之后,当前绘图坐标系就变化了,以后绘图都是基于更新的绘图坐标系了。也就是说,真正对我们绘图有用的是绘图坐标系而非Canvas坐标系。
- 总结就是:Canvas坐标系唯一不变,平常我们使用绘图坐标系。
Canvas常用Api
关于Canvas的save和restore
- save() : 用来保存Canvas的状态,save()方法之后的代码,可以调用Canvas的平移、放缩、旋转、裁剪等操作!
保存当前矩阵并剪辑到私有堆栈上。
随后对translation,scale,rotate,skew,concat或clipRect,clipPath的调用都将照常运行,但是在对restore()进行平衡调用时,这些调用将被忘记,并且在save()之前存在的设置也将被忘记。 将恢复。 - restore():用来恢复Canvas之前保存的状态(就是回到开始那个坐标轴状态),防止save()方法代码之后对Canvas执行的操作,继续对后续的绘制会产生影响,通过该方法可以避免连带的影响。
举个例子:比如我旋转坐标轴30度画一些东西,这时候原来的x、y相对旋转了30度,为了避免之后的绘制也在这个新坐标轴上画,使用restore回到sava的那个坐标轴。
drawtext
drawText (CharSequence text, int start, int end, float x, float y, Paint paint)
在指定的Paint中绘制由start / end指定的指定范围的文本,其原点为(x,y)。 根据对齐设置解释原点。画笔的对齐设置,默认是从左往右,还可以设置居中:
textPaint.setTextAlign(Paint.Align.CENTER);
//原点为(x,y)
drawText (String text, float x, float y, Paint paint)
drawTextOnPath (String text, Path path, float hOffset, float vOffset, Paint paint)
沿着指定的路径使用指定的画笔绘制以(x,y)为原点的文本。 绘画的对齐设置确定路径从何处开始文本。
//要求api达到23
drawTextRun (char[] text, int index, int count,
int contextIndex, int contextCount, float x,
float y, boolean isRtl, Paint paint)
在一个方向上绘制一行文本,并带有可选的上下文,以进行复杂的文本成形。
文本行中包含从头到尾的字符。另外,范围contextStart到contextEnd用作上下文,用于复杂的文本整形,例如阿拉伯文本可能根据其旁边的文本而具有不同的形状。
超出contextStart…contextEnd范围的所有文本都将被忽略。开始和结束之间的文本将被布局和绘制。上下文范围对于上下文整形很有用,例如紧缩,阿拉伯语语境形式。
文字方向由isRtl明确指定。因此,此方法仅适用于单方向行驶。文本的对齐方式由Paint的TextAlign值确定。此外,0 <= contextStart <= start <=结束<= contextEnd <= text.length必须在输入时保持。
Path
Path常用Api
Path经常用于自定义View,界面绘制,配合canvas.drawPath使用。
moveTo
moveTo用来移动画笔。移动过程不会有痕迹。
lineTo
用来画直线,默认是左上角(0,0)坐标。
quadTo
用来画二次贝塞尔曲线,起始默认点为(0,0)。
public void quadTo (float x1, float y1, float x2, float y2)
解释:从最后一个点开始添加一个二次贝塞尔曲线,逼近控制点(x1,y1),并在(x2,y2)处结束。 如果没有为此轮廓调用moveTo(),则第一个点将自动设置为(0,0)。
cubicTo
用来画三次贝塞尔曲线,起始默认点为(0,0)。
public void cubicTo (float x1, float y1, float x2, float y2, float x3, float y3)
解释:从最后一点添加一个三次方贝塞尔曲线,逼近控制点(x1,y1)和(x2,y2),并在(x3,y3)处结束。 如果没有为此轮廓调用moveTo(),则第一个点将自动设置为(0,0)。
注意事项:前2个点是控制点。
以A、B 2个点为例,B是结束点。先算出控制点F1和F2:x坐标为A、B重点,y坐标分别为A.y和B.y
arcTo
用来画圆弧,虽然贝塞尔曲线也能绘制,但是这个常用于截取(椭)圆的一部分。
public void arcTo(RectF oval, float startAngle, float sweepAngle)
解释:将指定的弧形作为新轮廓附加到路径。
如果路径的起点与路径的当前最后一个点不同,则将添加自动lineTo()以将当前轮廓连接到弧的起点。 但是,如果路径为空,则使用圆弧的第一点调用moveTo()。
例子:截取一个正方形的内切圆的右下角一段圆弧path
mRectF = new RectF(10, 10, 300, 300);
//3点钟方向开始,顺时针
mPath.arcTo(mRectF, 0, 90);
canvas.drawPath(mPath, mPaint);
public void arcTo(RectF oval, float startAngle, float sweepAngle,boolean forceMoveTo)
比上面的方法多了一个forceMoveTo变量。
分析一波:这个变量指的是,是否要和上一次操作的点连起来,设置为true,则始终以圆弧开始新轮廓,不管你path之前在哪个点,都直接从区域oval内开始绘制圆弧。如果为false,则会连起来,比如开始画圆时,坐标在(100,100),那么开始画圆弧之前会lineTo圆弧的起点。
api21出现的方法
public void arcTo (float left, float top, float right, float bottom, float startAngle, float sweepAngle, boolean forceMoveTo)
addRoundRect
向路径添加封闭的圆角矩形轮廓。
//此方法 rect和dir不能为空,必须有一个矩形,设置他的上圆角的x、y半径,最后设置缠绕矩形轮廓的方向
public void addRoundRect (RectF rect, float rx, float ry, Path.Direction dir)
api21出现的方法
//这个好理解,通过left、top等参数,省去了RectF的定义
public void addRoundRect (float left,
float top,
float right,
float bottom,
float rx,
float ry,
Path.Direction dir)
Direction 有顺时针和逆时针 CW、CCW
close
关闭当前轮廓。 如果当前点不等于轮廓的第一个点,则会自动添加线段。
reset
清除路径中的所有直线和曲线,使其为空。 这不会更改填充类型设置。
Paint
初始化
//也可以通过Paint#setFlags设置
Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
初始化相关flag
Paint.ANTI_ALIAS_FLAG :抗锯齿标志
Paint.FILTER_BITMAP_FLAG : 使位图过滤的位掩码标志
Paint.DITHER_FLAG : 使位图进行有利的抖动的位掩码标志
Paint.UNDERLINE_TEXT_FLAG : 下划线
Paint.STRIKE_THRU_TEXT_FLAG : 中划线
Paint.FAKE_BOLD_TEXT_FLAG : 加粗
Paint.LINEAR_TEXT_FLAG : 使文本平滑线性扩展的油漆标志
Paint.SUBPIXEL_TEXT_FLAG : 使文本的亚像素定位的绘图标志
Paint.EMBEDDED_BITMAP_TEXT_FLAG : 绘制文本时允许使用位图字体的绘图标志
Paint常用Api
Paint#setTextAlign
对齐设置
//居中
setTextAlign(Paint.Align.CENTER)
Paint#setShader
为Paint添加渐变器。
以线性渐变为例
//为Paint设置渐变器
hader mShader = new LinearGradient(0,0,40,60,
new int[]{
Color.RED,Color.GREEN,Color.BLUE,Color.YELLOW},
null,Shader.TileMode.REPEAT);
//为Paint设置渐变器
paint.setShader(mShader);
关于LinearGradient
/**
* x0 起点x坐标
* y0 起点y坐标
* x1
* y1
* colors 渐变颜色集合
* positions 颜色数组中每种相应颜色的相对位置[0..1]。如果为null,则颜色会沿着渐变线均匀分布。 该值可以为空。
* tile 着色器拼贴模式,此值不得为null。{Shader.TileMode.CLAMP、Shader.TileMode.REPEAT}
**/
public LinearGradient (float x0,
float y0,
float x1,
float y1,
int[] colors,
float[] positions,
Shader.TileMode tile)
Paint#setPathEffect
设置Paint画出虚线
//设置画笔画出虚线 PathEffect使用
//两个值分别为循环的实线长度、空白长度
float[] f = {dp2pxF(5f), dp2pxF(2f)};
PathEffect pathEffect = new DashPathEffect(f, 0);
paint.setPathEffect(pathEffect);
/**
* dp转pxF
* MyApplication为工程自定义application类
*/
public static float dp2pxF(float dpValue) {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpValue, MyApplication.getInstance().getResources().getDisplayMetrics());
}
Paint#setTypeface
可以设置自定义字体
Typeface createFromAsset(AssetManager mgr, String path) //通过从Asset中获取外部字体来显示字体样式
Typeface createFromFile(String path)//直接从路径创建
Typeface createFromFile(File path)//从外部路径来创建字体样式
示例
//...省略其他代码
Typeface typeface=Typeface.createFromAsset(mContext.getAssets(), "fonts/hwxk.ttf");
mPaint.setTypeface(typeface);
canvas.drawText(text, 10,100, mPaint);
获取文字宽度
//...省略其他代码
String str = "Hello";
//1. 粗略计算文字宽度
Log.d(TAG, "measureText=" + paint.measureText(str));
//2. 计算文字所在矩形,可以得到宽高
Rect rect = new Rect();
paint.getTextBounds(str, 0, str.length(), rect);
int w = rect.width();
int h = rect.height();
Log.d(TAG, "w=" +w+" h="+h);
//3. 精确计算文字宽度
int textWidth = getTextWidth(paint, str);
Log.d(TAG, "textWidth=" + textWidth);
public static int getTextWidth(Paint paint, String str) {
int iRet = 0;
if (str != null && str.length() > 0) {
int len = str.length();
float[] widths = new float[len];
paint.getTextWidths(str, widths);
for (int j = 0; j < len; j++) {
iRet += (int) Math.ceil(widths[j]);
}
}
return iRet;
}
View的事件分发
参考我之前写的一篇文章理解事件分发中的3个方法
onTouch和onTouchEvent
①如果onTouch()方法返回值是true(事件被消费)时,则onTouchEvent()方法将不会被执行;
②只有当onTouch()方法返回值是false(事件未被消费,向下传递)时,onTouchEvent方法才被执行。
③平时我们使用的OnClickListener,其优先级最低,即处于事件传递的尾端。(实现都基于onTouchEvent)
④给View设置监听OnTouchListener,重写onTouch()方法。其优先级比onTouchEvent()高。如果不返回false,那么设置的点击监听等也就意味着失效了。
自定义View
注意事项
- 不要在onDraw或是onLayout()中去创建对象,因为onDraw()方法可能会被频繁调用,可以在view的构造函数中进行创建对象;
- 降低view的刷新频率。尽可能减少不必要的调用invalidate相关的方法。
- 打开硬件加速可以带来性能提升。
- 状态恢复与保存。部分原生控件源码中有做状态恢复与保存的,自定义View需要自行判断是否要添加状态恢复与保存。
- 在View#onDetachedFromWindow中停止动画或线程。
自定义属性
- 使用 TypedArray来获取布局文件中属性的值,使用完毕后调用 recyle() 方法回收TypedArray对象。
- 获取属性值的操作一般写在View的构造方法中,在之后View的绘制流程里可以使用到这些值。
- 添加自定义命名空间,推荐使用res-auto
举例
//app可以改成你想的任意名字
xmlns:app="http://schemas.android/apk/res-auto"
- values下添加attrs.xml,添加自定义属性
<declare-styleable name="RoundishImageView">
<attr name="cornerRadius" format="dimension" />
<attr name="roundedCorners">
<flag name="topLeft" value="1" />
<flag name="topRight" value="2" />
<flag name="bottomRight" value="4" />
<flag name="bottomLeft" value="8" />
<flag name="all" value="15" />
</attr>
</declare-styleable>
- 重写类的构造方法,实现必须实现的onDraw方法。(其他方法看情况)
滑动冲突的处理
- 外部拦截
(onInterceptTouchEvent),如果父容器需要则拦截,如果不需要则不拦截,称为外部拦截法。
- 内部拦截
父容器不拦截任何事件,将所有事件传递给子元素,如果子元素需要则消耗掉,如果不需要则通过requestDisallowInterceptTouchEvent方法(请求父类不要拦截,返回值为true时不拦截,返回值为false时为拦截)交给父容器处理,称为内部拦截法.
- RecyclerView辅助类SnapHelper使用
- 5.0 Material Design中CoordinateLayout+AppBarLayout+CollasingToolBarLayout的使用解决一部分滑动冲突。
- ViewPager2滑动冲突最全处理方案
SurfaceView
SurfaceView中采用了双缓冲机制,保证了UI界面的流畅性,同时SurfaceView不在主线程中绘制,而是另开辟一个线程去绘制,所以它不妨碍UI线程;
SurfaceView继承于View,他和View主要有以下三点区别:
- View底层没有双缓冲机制,SurfaceView有;
- view主要适用于主动更新,而SurfaceView适用与被动的更新,如频繁的刷新;
- view会在主线程中去更新UI,而SurfaceView则在子线程中刷新;SurfaceView的内容不在应用窗口上,所以不能使用变换(平移、缩放、旋转等)。也难以放在ListView或者ScrollView中,不能使用UI控件的一些特性比如View.setAlpha() ;
View:显示视图,内置画布,提供图形绘制函数、触屏事件、按键事件函数等;必须在UI主线程内更新画面,速度较慢。
SurfaceView:基于view视图进行拓展的视图类,更适合2D游戏的开发;是view的子类,类似使用双缓机制,在新的线程中更新画面所以刷新界面速度比view快,Camera预览界面使用SurfaceView。
GLSurfaceView:基于SurfaceView视图再次进行拓展的视图类,专用于3D游戏开发的视图;是SurfaceView的子类,openGL专用。
参考:5.0源码中的SurfaceView、TextureView的介绍
系统原理
apk包建立过程(build process)
精简版
名词解释
- aapt:Android Asset Packaging Tool
- compiler:编译器
流程
- 通过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文件 的速度会更快。
资源冲突
- 第三方资源和工程资源有冲突,比如都是用了support包,但是版本不一造成问题
api (某第三方资源)
{
exclude group: 'com.android.support'
}
api (某第三方资源)
{
//假如没有使用到库中的glide,加上这句移除对Glide的依赖以减小包体积
exclude module: 'glide'
}
- R文件中id冲突
每个资源都对应一个R中的16进制,由三部组成,packageId,TypeId,EntryId。
- packageId:apk包的id,默认为0x7f。
- TypeId:资源类型id,如layout,string,ID,drawable等。
- EntryId:类型TypeId下面的资源id,从0开始递增。
解决这种冲突,涉及到Andoird资源的编译和打包原理。市面上基本都是通过修改aapt的产物或者定制aapt来解决。
安装流程
安装过程其实非常复杂,下面先记录一下简化后的流程
- 复制APK到/data/app目录下,解压并扫描安装包。
- 资源管理器解析APK里的资源文件。
- 解析AndroidManifest文件,并在/data/data/目录下创建对应的应用数据目录。
- 然后对dex文件进行优化,并保存在dalvik-cache目录下。
- 将AndroidManifest文件解析出的四大组件信息注册到PackageManagerService中。
- 安装完成后,发送广播。
加载图片过程
proguard和R8
Proguard是一个集文件压缩,优化,混淆和校验等功能的工具
- 压缩(Shrinking)
- 优化(Optimization)
- 混淆(Obfuscation)
- 预校验(Preverification)Android无效
混淆
作用:混淆将主项目及依赖库中未被使用的类,类成员,方法,属性移除,有助于规避64K方法数的瓶颈,会删除无用的资源,有效的减小apk包的大小,同时将类及其成员,方法重命名为无意义的简短名称,增加逆向工程的难度。
Android Studio 3.4 或 Android Gradle 插件 3.4.0 及更高版本时,R8是默认编译器,用于将项目的 Java 字节码转换为在 Android 平台上运行的 DEX 格式。3.4.0之前老版本插件使用 ProGuard 。
R8 在每次运行时都会创建一个mapping.txt文件,其中列出了经过混淆处理的类、方法和字段名称与原始名称的映射关系。(/build/outputs/mapping// 目录)
android {
buildTypes {
release {
// Enables code shrinking, obfuscation, and optimization for only
// your project's release build type.
minifyEnabled true
// Enables resource shrinking, which is performed by the
// Android Gradle plugin.
shrinkResources true
// Includes the default ProGuard rules files that are packaged with
// the Android Gradle plugin. To learn more, go to the section about
// R8 configuration files.
proguardFiles getDefaultProguardFile(
'proguard-android-optimize.txt'),
'proguard-rules.pro'
}
}
...
}
ProGuard官方使用手册:ProGuard使用
可阅读:代码混淆做了啥
总结
因为是多方决定的最终混淆文件,所以也不列举例子了,具体怎么写可以查看ProGuard官方文档。
规则文件来源 | 描述 |
---|---|
Android Gradle 插件 | 在编译时,由 Android Gradle 插件生成 |
AAPT2 | 在编译时,AAPT2 根据对应用清单中的类、布局及其他应用资源的引用来生成保留规则 |
Module | 创建新 Module 时,由 IDE 创建,或者另外按需创建 |
将 minifyEnabled 属性设为 true,ProGuard 或 R8 会将来自上面列出的所有可用来源的规则组合在一起
。为了看到完整的规则文件,可以在proguard-rules.pro 中添加以下配置,输出编译项目时应用的所有规则的完整报告:
-printconfiguration build/intermediates/proguard-files/full-config.txt
混淆jar包
- 写一个规则文件(proguard-rules.pro)
-injars 'E:\myjar.jar' #需要混淆的jar文件和路径
-outjars 'E:\myjar_out.jar' #混淆后的jar文件名字和路径
-libraryjars 'D:\Sdk\platforms\android-29\android.jar' #jar依赖的包
- 去sdk目录下找到tools\proguard\bin\proguardgui.bat,直接运行
- 照着提示一步步来
网络请求
网络库
- Google官方文档上有Volley和Cronet的介绍。
https://developer.android/training/volley?hl=zh-cn
- okhttp用起来非常便捷,但是是java语言实现,无法跨平台。
- retrofit内部使用的okhttp,他通过使用大量的设计模式进行功能模块的解耦,方便了写程序的人。
- 对大型应用来说跨平台是非常重要,所以许多公司使用的Mars和Cronet。
OkHttp
背景:自从Android4.4开始,google已经开始将源码中的HttpURLConnection替换为OkHttp,而在Android6.0之后的SDK中google更是移除了对于HttpClient的支持,而市面上流行的Retrofit同样是使用OkHttp进行再次封装而来的。
以okhttp3为例,请求流程见图
- 可见不管同步异步,其实都调用到了RealCall#execute()方法(Call是个接口类)
- 自定义拦截器级别高于其他拦截器
图片
图片加载库
Android-Universal-Image-Loader
老牌图片加载库。2011年项目开始维护,2015停止维护。对于那个时候的安卓生态圈来说,ImageLoader真的挺牛的。
Volley
Volley作为网络请求库,还能用于图片加载。Google官方文档上有介绍。
- Volley可以进行图片的加载和缓存,可以利用ImageRequest对象简单、方便地进行网络图片的获取。
- ImageLoader内部就是用的ImageRequest来实现的。
- NetworkImageView是Volley提供的一个自定义View,可直接设置网络图片。
参考:使用Volley加载网络图片
Picasso
GitHub: https://github/square/picasso
和Square的网络库一起能发挥最大作用,因为Picasso可以选择将网络请求的缓存部分交给了okhttp实现。毕竟是Square公司出品,JakeWharton大神带头研发的。Picasso不支持gif。优点是体积小,如果没有特别的需要,可以选择他。(Square全家桶干活)
Glide
GitHub: https://github/bumptech/glide
Picasso能干的,Glide都能干,Glide能干的,Picasso不一定能干。
优点
- 声明周期集成
- 高效处理Bitmap,这点Fresco做的不够好
- 使用简单
- 可扩展性强
v4配置
v3版本的Glide中默认使用HttpUrlConnection来加载网络图片。如果你应用中添加了OkHttp,Glide会自动合并使用Okhttp来加载网络图片。如果项目里添加2个网络库,比如OkHttp和Volley,那Glide就处于一个不稳定状态,用哪个网络库看心情。所以自己实现一个AppGlideModule进行配置就显得很重要了。
虽然v4应该改了不少默认配置,但是自定义AppGlideModule还是有必要写的。。
注意事项
- 避免在程序库中使用 AppGlideModule。一个app里面,如果有多套配置,根本过不了编译,必须要要做出取舍。
Fresco
GitHub: https://github/facebook/fresco
官方文档: https://www.fresco-cn/docs/index.html
如果你的应用对图片的显示、加载等要求高的话,那就建议使用Fresco。
优点
- 内存自动回收。图片不可见时,会及时自动释放所占用的内存,尽可能地避免OOM( 在更底层的Native层对OOM进行)
- 三级缓存机制。两级内存缓存(解码的与未解码的)+一级磁盘缓存,提升加载速度,节省内存占用空间
- 支持各种加载场景。如动图加载、高斯模糊等常见的图片加载场景。另外还提供了独特的渐进式加载、先加载小图再加载大图,加载进度等功能(很强大)。
最大的优势在于5.0以下(最低2.3)的bitmap加载。在5.0以下系统,Fresco将图片放到一个特别的内存区域(Ashmem区)
大大减少OOM(在更底层的Native层对OOM进行处理,图片将不再占用App的内存)
适用于需要高性能加载大量图片的场景
缺点
- 体积大。较其他主流图片库体积要大不少。
- 侵入性较强。须使用它提供的SimpleDraweeView来代替ImageView加载显示图片
图片选择器
GitHub上有很多,我自己也封装过一个Android图片选择器PhotoPicker。实现图片选择器不外乎2点:图片加载库+系统api。
数据解析
json
一提到json解析,马上想到Google的Gson库和阿里巴巴的fastjson库,各有优点。
没有性能要求,可以使用google的Gson,如果有性能上面的要求可以使用Gson将bean转换json确保数据的正确,使用FastJson将Json转换Bean。
xml
SAX、Pull、Dom
屏幕适配
屏幕尺寸
手机对角线的物理尺寸 单位:英寸(inch),1英寸=2.54cm。常见5寸、5.5寸、6寸。
屏幕分辨率
手机在横向、纵向上的像素点数总和。常见320x480、480x800、720x1280、1080x1920、1080*2340。
px、dp、dpi、ppi、density
ppi等于或约等于dpi。ppi是屏幕的物理参数,叫像素密度,每英寸上像素数目。
dpi也叫像素密度。与ppi不同,dpi能被人为调整。系统指定单位尺寸上像素数量。
有几部手机分辨率相同,但是尺寸不同,他们的dpi相同,但是ppi不同。
密度density,值为dpi/160
dp是个抽象单位
density=dpi/160
dp*density=px
常见的适配方案
- AutoLayout库
- smallestWidth 限定符适配(生成不同分辨率的资源文件,布局中引用)
- AndroidAutoSize库(反射修正系统的density值,可直接使用dp)
除了适配方案,在平时写代码的时候也需要注意,比如不要使用绝对布局,多使用百分比布局;使用.9图;使用约束布局;
性能优化
想一想在我几年的工作中,并没有很多机会进行性能优化。原因是我太懒了,再就是领导对应用的要求不高。在看大佬文章之前,我印象中的性能优化大概就是布局优化、包体积优化、内存优化、耗电优化这几个,我是这么做的:
-
布局优化
布局层次能少则少,多一层布局则多一层渲染,耗时增加。
使用约束布局,效率高,能减少布局层次。还可以通过代码构建布局,减少耗时:用kotlin的dsl构建布局,用时缩短了20倍
ViewStub占位符使用, 标签实质上是一个宽高都为 0 的不可见 View. 通过延迟加载布局的方式优化布局提升渲染性能。
把常用的布局抽出来,进行布局复用。常用和,merge很好理解:使用include引入子布局时,子布局最外层标签是merge,可以减少一层嵌套。 -
包体积优化
混淆:合理的混淆不仅能增加apk的安全性,还能减少代码量,减小apk的体积。
删除不必要的文件和代码:使用lint等检测工具检查资源文件。
良好的代码风格:这么说把,同一个功能,用最合适的数据结构和算法,代码量会大大减少,不仅会减小耗电量以及内存的损耗,还减小了应用体积,虽然几行代码并不多,但减小1字节就不是包体积优化? -
内存优化和耗电优化
首先多看多记多实践。多了解几种数据结构的使用,多做点算法题,都不是坏事。
还要避免犯一些低级错误比如:
①引用持有引用,导致引用无法被回收。
②资源使用完毕没手动关闭,比如文件流、IO流、Cursor等。
此部分知识链接全来自“极客时间”,张绍文大佬的课程《Android开发高手课》,这是我买的最值的课。
奔溃优化
https://time.geekbang/column/article/70602
内存优化
https://time.geekbang/column/article/71277
卡顿优化
https://time.geekbang/column/article/71277
启动优化
https://time.geekbang/column/article/73651
I/O优化
https://time.geekbang/column/article/74988
存储优化
https://time.geekbang/column/article/76677
网络优化
https://time.geekbang/column/article/77990
LRU 的原理
为减少流量消耗,可采用缓存策略。常用的缓存算法是LRU(Least Recently Used):当缓存满时, 会优先淘汰那些近期最少使用的缓存对象。主要是两种方式:
- LruCache(内存缓存)
LruCache 类是一个线程安全的泛型类:内部采用一个LinkedHashMap以强引用的方式存储外界的缓存对象,并提供get和put方法来完成缓存的获取和添加操作,当缓存满时会移除较早使用的缓存对象,再添加新的缓存对象。
- DiskLruCache(磁盘缓存)
通过将缓存对象写入文件系统从而实现缓存效果。
耗电优化
https://time.geekbang/column/article/79642
UI优化
https://time.geekbang/column/article/80921
包体积优化
https://time.geekbang/column/article/81202
热门技术
组件化
概念:是将一个APP分成多个module,每个module都是一个组件,也可以是一个基础库供组件依赖,开发中可以单独调试部分组件,组件中不需要相互依赖但是可以相互调用,最终发布的时候所有组件以lib的形式被主APP工程依赖打包成一个apk。
优势
- 方便单元测试
- 利于维护。
- 模块独立,可统一管理。
- 实现了独立编译和独立打包。省了不少编译打包时间。
- 业务模块可快速拆封,用于其他项目。
- 并行开发,降低耦合。每个人只需专注于自己负责的模块。
在工程build.gradle加一个开关控制moudle作为lib被主APP依赖还是作为独立APP
ext {
//true 每个业务Module可以单独开发
//false 每个业务Module以lib的方式运行
isModule = false
}
在moudle的build.gradle中
if (Boolean.valueOf(rootProject.ext.isModule)) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
//...省略
android {
//...省略
sourceSets {
main {
if (Boolean.valueOf(rootProject.ext.isModule)) {
//新建配置文件module/AndroidManifest.xml,Moudle作为独立APP时使用
manifest.srcFile 'src/main/module/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
java {
//全部module一起编译时,剔除module目录
exclude '**/module/**'
}
}
}
}
}
注意事项
- 资源冲突,module独立开发的时候可能没问题,一旦合并,就可能出现资源文件名冲突,所以在命名的时候要注意。
- 依赖关系,可以把工具类和公用的library写在BaseModule,供每个组件使用。
- 定义一个BaseApplication,其它的Application直接继承此BaseApplication。
- 组件通信,可以使用ARouter
插件化&&热修复
热修复java层面切入实践
Android插件化原理
插件化
四大组件的插件化是插件化技术的核心知识点。
插件化是体现在功能拆分方面的,它将某个功能独立提取出来,独立开发,独立测试,再插入到主应用中。以此来减少主应用的规模。插件化只是增加新的功能类或者是资源文件,所以不涉及抢先加载旧的类这样的使命,就避过了阻止相关类去打上CLASS_ISPREVERIFIED标志和还有在热修复时动态改变BaseDexClassLoader对象间接引用的dexElements.
HostApp:壳app
PluginApp:插件app
注意事项
- 插件化开发的APP不能在Google Play上线
- Hook、反射、接口,现在主流都是使用Hook
- 资源冲突处理,aapt的定制或者使用。
- 插件化比热修复简单,热修复是在插件化的基础上在进行替旧的Bug类。
热修复
热修复是体现在bug修复方面的,它实现的是不需要重新发版和重新安装,就可以去修复已知的bug。热修复因为是为了修复Bug的,所以要将新的同名类替代同名的Bug类,要抢先加载新的类而不是Bug类,所以多做两件事:在原先的app打包的时候,阻止相关类去打上CLASS_ISPREVERIFIED标志,还有在热修复时动态改变BaseDexClassLoader对象间接引用的dexElements,这样才能抢先代替Bug类,完成系统不加载旧的Bug类
贴几个热修复解决方案:
多渠道打包
关键词:gradle文件配置、母包、子包
跨端应用
跨平台开发以后肯定会是主流,实现跨平台的框架与很多,各有优势缺点。毕竟环境比较复杂,考虑的要素也多。
1.Hybird
产生背景
移动互联网的热潮的兴起的时候,许多公司为了降低开发成本和抢占移动移动应用市场。市场需要技术,Hybird App应运而生。
简介
APP经历了从本地化应用(Native App),到基于WEB的应用Web App,再到混合型应用(Hybrid APP)的过程。
Hybrid App(混合型应用)是指介于web-app、native-app这两者之间的app,Hybrid app从外观上来看是一个native app(因为有用到native的壳),内部又是使用web-app,如新闻类和视频类的应用普遍采取该策略:native的框架加上web的内容。不同于native app需要针对不同的平台使用不同的开发语言(如使用Objective-C、Swift开发iOS应用,使用Java、Kotlin开发Android应用,使用C#开发Windows Phone应用),hybrid app允许开发者仅使用一套网页语言代码,即可开发能够在不同平台上部署的类原生应用 (网页编程语言有很多,如HTML、JavaScript、PHP,我们需要从中选择我们需要使用的一种或几种来开发Hybrid App,比如HTML5+JavaScript)。
Hybrid App的本质,其实是在原生的 App 中,使用 WebView 作为容器直接承载 Web页面。因此,最核心的点就是 Native端 与 H5端 之间的双向通讯层,其实这里也可以理解为我们需要一套跨语言通讯方案,来完成 Native(Java/Objective-c/…) 与 JavaScript 的通讯。这个方案就是我们所说的 JSBridge,而实现的关键便是作为容器的 WebView,一切的原理都是基于 WebView 的机制。
分类
Hybird有很多种分类,多View和单View混合型是偏向于原生应用的,通常不能跨平台;而Web主体型和复合型是偏向于网页应用的,可以跨平台。
补充
Web App(网页应用) | Hybrid App(混合应用) | Native App(原生应用) | |
---|---|---|---|
开发成本 | 低 | 中 | 高 |
维护更新 | 简单 | 简单 | 复杂 |
体验 | 差 | 中 | 优 |
Store或market认可 | 不认可 | 认可 | 认可 |
安装 | 不需要 | 需要 | 需要 |
跨平台支持 | 支持 | 一部分支持 | 不支持 |
开发语言 | 只使用web编程语言 | 原生开发语言+web | 只使用原生开发语言 |
Cordova
Apache Cordova是一个开源的移动开发框架。允许你用标准的web技术-HTML5,CSS3和JavaScript做跨平台开发。Cordova的命令行运行在Node.js 上面并且可以通过NPM安装。常用于Hybird App开发。
详情见:Cordovaz中文网
2.React Native
GitHub地址:https://github/facebook/react-native
3.Flutter
Flutter官方中文网
Flutter已经和Dart语言绑定了。Dart 是目前唯一一个支持严格的 AOT 和 JIT 的编程语言。
Flutter非常火,有兴趣可以看看2019GMTC全球大前端技术大会上任晓帅的演讲《Dart is All The Things》。
4.Hippy
可以看一下 2019GMTC全球大前端技术大会上李思广的演讲《多端一体化框架 Hippy 的开放与未来》。
利于高效开发的一些插件和库记录
adb
配置adb环境变量
为啥要配置环境变量,很简单,不配置就只能在Android Studio里的terminal里使用adb命令,配置后你可以在任意文件夹里通过调出命令提示符来使用adb命令。
- 找你的sdk的路径,把platform-tools和tools的绝对路径加到系统环境变量Path里就行了。(可以新建一个叫adb的系统变量,然后通过分号把上面2个路径加入,最后在Path里添加%adb%)
常用命令
adb
查看版本号
adb devices
显示当前运行的全部模拟器
adb install -r -t xx.apk
安装当前路径下的xx.apk
卸载apk包
adb uninstall <apk的主包名>
adb pull
获取模拟器中的文件
adb push
向模拟器中写文件(确保文件系统不是 Read-only file system,不然push会失败)
adb shell
进入shell模式
进入shell模式键入cd data/app,然后通过“ls //”查看文件列表,部分shell是Permission denied。
1、你的手机是root过的或者是一台这样的机器.(ADP1 or an ION from Google I/O)
2、以root模式运行adb,输入adb root.
#cd system/sd/data //进入系统内指定文件夹
#ls //列表显示当前文件夹内容
#rm -r xxx //删除名字为xxx的文件夹及其里面的所有文件
#rm xxx //删除文件xxx
#rmdir xxx //删除xxx的文件夹
adb shell wm size
查看分辨率
adb help
adb logcat -s <标签名>
在命令行中查看LOG信息
adb logcat -s TAG:*
debug模式
adb shell am start -n 包名/包名+类名(-n 类名,-a action,-d date,-m MIME-TYPE,-c category,-e 扩展数据,等)。
通过命令打开某个Activity如果报错:Security exception: Permission Denial
那就要去配置文件里看,这个Activity有没有intent-filter属性。Android组件中有个exported属性,当组件没有intent-filter时exported属性默认为false,此组件只能由本应用用户访问,配备了intent-filter后此值改变为true,允许外部调用。
adb get-product
获取设备ID
adb get-serialno
获取设备序列号
adb wifi使用
首先确保电脑和手机是一个局域网里的。
- 手机通过USB与电脑先连上。
- 在命令提示符下运行
adb tcpip 5555
- 获取手机ip地址,在命令提示符下运行
adb shell "ip addr show wlan0 | grep -e wlan0$ | cut -d\" \" -f 6 | cut -d/ -f 1"
- 断开usb,并且在命令提示符下运行
adb connect <ip_address>:5555
接下来你就可以不用连线调试app了。
adb 连接常用的电脑端模拟器
调出命令提示行工具,切换到sdk的platform-tools目录(如果系统环境变量有sdk下platform-tools的路径,则可直接调出命令提示行),使用如下命令,端口号可能在以后发生变化,具体看这些模拟器的开发商。
网易MUMU模拟器:adb connect 127.0.0.1:7555
夜神模拟器:adb connect 127.0.0.1:62001
逍遥安卓模拟器:adb connect 127.0.0.1:21503
天天模拟器:adb connect 127.0.0.1:6555
海马玩模拟器:adb connect 127.0.0.1:53001
//雷电模拟器用的adb默认端口5555,基本上打开模拟器就连上了,此外多开的模拟器,端口号在原来基础+1
//比如 adb connect 127.0.0.1:5556
雷电模拟器:adb connect 127.0.0.1:5555
DI
组件只管依赖的使用,而依赖的具体实现交给容器去决定,这是DI(Dependency Injection)框架的核心思想。
参考项目:google/dagger
使用Dagger2不用担心性能的消耗,不使用反射,所有的注解均停留在编译时期。
数据库
使用 Room 将数据保存到本地数据库
greendao使用记录
EventBus
EventBus是一款在android开发中使用的发布/订阅事件的总线框架,基于发布订阅者模式。
EventBus我之前用得多,后面一直用Rxbus。也又很多人使用LiveData封装了事件总线用于开发,GitHub上很多。
databinding
早期的mvvm架构项目,基本使用的databinding。注意一点:databinding 不是基于函数式编程的数据驱动 UI 框架,而是通过 “自动化生成中间代码”来实现的。并没有遵循函数式编程。
可查看我写的:databinding实践
gradle
Gradle是一个构建工具,它是用来帮助我们构建app的,构建包括编译、打包等过程。
Gradle 是基于groovy语言实现(基于JVM的语法和java类似的脚本语言)的一个Android编译系统, google针对Android编译用groovy语言开发了一套dsl,这就是gradle。 因此,遇到不明白的gradle配置,直接看看相关groovy的源码,一般都可以找到解决的办法。
jetpack
2018年谷歌I/O 发布了一系列辅助android开发者的实用工具,合称Jetpack,以帮助开发者构建出色的 Android 应用。jetpack家族很庞大,值得深入研究。
Jetpack库可单独使用,也可以组合使用,满足不同的需求。比如Jetpack MVVM 的边界目前仅限于 LifeCycle、LiveData、ViewModel、DataBinding 这四样。Jetpack Room实现数据存储持久性。Jetpack CameraX满足相机应用需求。Jetpack Navigation 管理应用导航流程。
扔几篇好文:
重学安卓:从 被误解 到 真香 的 Jetpack DataBinding!
重学安卓:有了 Jetpack ViewModel . . . 真的可以为所欲为!
重学安卓:是让人耳目一新的 Jetpack MVVM 精讲啊!
jni
JNI,全称为Java Native Interface,即Java本地接口,通过使用 Java本地接口书写程序,可以确保代码在不同的平台上方便移植。
JNI使用记录
JNI装载库文件load和loadLibrary浅析
关于NDK如何生成so文件
rxjava
响应式编程、链式调用,rxjava是个方便的东西。学习rxjava的关键在使用操作符。操作符的本质是声明式编程。与命令式不一样的是,声明式编程只交待做什么,但无须交待怎么做。有点像SQL语句,按一定的规则,向数据库中的数据声明你要做什么(增删改查)。
rxjava也该这么学习,对于各类操作符,我们只需要关心输入数据、变换规则和输出数据。
rxjava实质是函数式编程,因为可以看作多个纯函数的链式调用。不在方法中直接调用和修改外部成员,只关注入口和出口,中间过程一气呵成。函数式编程从范式层面彻底解决了过程的一致性问题,是一种值得遵循的编程规范。因为任何对全局变量的修改,都会对引用到他的地方产生影响,任何一个空指针异常都可能造成程序的崩溃。
//关于map是如何变换数据,filter是如何限制数据,我们无需关心
Observable.just(1, 3, 5, 7, 9)
.map(i -> i + 1)
.filter(i -> i < 5)
.subscribe(getObserve());
RxJava2专栏
RxJava1系列文章开篇
反编译
apktool,dex2jar,jd-gui简单使用
apk解包修改后重新打包
测试
Robotium、Robolectric等测试框架;
Google的UI测试框架Espresso;
架构、设计模式、框架
架构范围最大
对比
框架与架构
架构比较抽象,框架比架构更具体,更偏向于实现某种“方便”,1个架构可以通过使用多种框架来实现。
框架与设计模式
1种设计模式可以被不同框架和不同语言实现,框架是多种设计模式和代码的混合体。
架构与设计模式
设计模式是用于解决一种特定的问题,范围较小;架构针对体系结构进行设计,范畴较大。一个架构里可能有多个框架和多个设计模式,为的都是让框架更加稳定。
Android中常用的架构模式
MVC、MVP、MVVM
MVP 和 MVVM 二者之间没有任何关系。MVP 的出现与MVC离不开(为了解决MVC的一些问题,是MVC的升级),而 MVVM 是现代化软件开发模式的范例。
关键字:视图、模型、业务逻辑处理、数据逻辑处理、界面交互、视图事件。
MVC
不使用架构进行开发,带来的问题是 各活动窗口及碎片逻辑臃肿,不利于扩展。MVC作用就是解耦。减少控制逻辑、数据逻辑和界面交互的耦合度。
MVP
作用也是解耦,切断了View和Model之间的联系,用Presenter充当桥梁。MVP是基于适配器模式,在MVC 模式泛滥的背景下,遵循依赖倒置原则以便能够随时替换 V 和 M 的一种实现。
优点:
- 结构清晰,职责划分清晰。
- 模块间充分解耦。
- 有利于组件的重用。
缺点
会引入大量的接口,导致项目文件数量激增;P层里的代码量会比较大,P持有M层和V层引用对象,M和V发生改变(体现在接口的变化)时,需要同步。需要注意异步调用情况下,页面关闭调用View层方法空指针等。
MVVM
作用也是解耦。同时将 MVC 中的 View 和 Model 解耦以及 MVP 中 Presenter 和 View 解耦。
ViewModel 也不会持有 View。其中 ViewModel 中的改动,会自动反馈给 View 进行界面更新,而 View 中的事件,也会自动反馈给 ViewModel。ViewModel只负责状态变量本身的变化,其他的不管(比如变量被哪些视图绑定,有没有绑定等)。
通常配合一些库使用,比如 databinding。
参考链接
重学安卓:Activity 的快乐你不懂!
Android 操作系统架构开篇
Android系统架构与系统源码目录
写给 Android 应用工程师的 Binder 原理剖析
Activity的启动模式
任务和返回栈
代码混淆到底做了什么?
Android 开发中的架构模式
笔者的感悟
android的水太深,人的精力有限,建议多选择那些半衰期长的知识学习。就我个人而言,最近在学kotlin,学了点皮毛之后去看别人的项目,发现这里这个关键字没见过,那里那个类又不知道干嘛的。我直接放弃,转过头看基础知识。在学习kotlin过程中,我发现有许多地方和JavaScript语法相似,理解起来也比较轻松。
版权声明:本文标题:Android基础知识梳理 内容由热心网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:https://m.elefans.com/xitong/1727654995a1123832.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
发表评论