图片缓存
布局加载
inflater.inflate(R.layout.item, null);
inflater.inflate(R.layout.item, null, false);
inflater.inflate(R.layout.item, null, true);
当父布局为null时,第一个参数的layoutparams布局参数无效(没有设置layoutparams)
inflater.inflate(R.layout.item, parent, true);
第一个参数的布局直接添加到了父布局中,外部参数有效
inflater.inflate(R.layout.item, parent, false);
外部布局参数有效,但没有添加到父布局中
canvas的save和restore要配对使用,save用于保存当前画布的裁切变换状态,restore用于恢复到上次save时的状态
canvas相当于一个绘制区域
任何操作对已经绘制过的内容不受影响
图片内存占用:bitmap width * bitmap height * 每个像素的内存占用(磁盘,文件,流等)
res资源文件夹不同也会导致内存占用不同
与图片显示尺寸无关
Android7.0 严格了进程间的文件共享,进程间传递uri,需要配置fileporvider
Android8.0 通知增加了channel;应用内安装需要增加权限;隐式广播将无法被静态注册的接收器收到
Android9.0 http禁用;不能直接在非 Activity 环境中启动 Activity,需要加flag;启动前台服务需要加权限
Android10 沙盒化存储,操作沙盒中的数据不需要权限,操作沙盒外的数据需要,并且需要使用ContentResolver和MediaStore来操作
单选框和多选框按钮状态改变是会有回调
单选clearcheck会回调失焦的按钮
radiogroup监听回调当前选中的radiobutton
SharedPreferences
SharedPreferences是线程安全的,它的内部实现使用了大量synchronized关键字
第一次调用getSharedPreferences会加载磁盘 xml 文件(这个加载过程是异步的,通过new Thread来执行,所以并不会在构造SharedPreferences的时候阻塞线程,但是会阻塞getXxx/putXxx/remove/clear等调用),以一个mLoaded变量来标记一个sp是否初始化完成,如果mLoaded == false,读写操作主线程会wait,初始化完成后mLoaded = true,然后会notifyAll其他等待线程
读操作会先加锁,然后判断sp是否初始化完成,没有完成会wait,然后直接读取内存中数据
写操作会有三把锁,putXXX()会先放到一个mEditorMap中,此过程加锁,commit或者apply时读mEditorMap一个锁,写xml一个锁
commit先合并map,然后同步写xml
apply先合并map,然后启动一个全局HandlerThread执行写xml
apply为什么会导致anr,apply会创建一个等待锁放到QueuedWork中,任务完成会清除对应的等待锁,在activity onStop()时会检查QueuedWork是否还有等待锁,有的话就执行等待锁await,直到任务完成会释放锁。今日头条团队采取了清空等待队列的方式,因为队列看起来只是一个监控任务有没有完成的作用,清空后对apply的失败率并没有影响
apply()会同步更新内存中的值,调用后立马修改
Android组件化笔记
多工程项目,分为基础模块,基础功能模块,功能模块为独立app,模块之间使用maven依赖,aar只包含当前项目代码,依赖的源码路径在pom文件中指明
基础功能模块使用SNAPSHOT版本声明,基础模块有更新时子模块都可以拿到最新版本(需满足缓存超时的前提,默认为24小时)
动态版本管理倾向于积极拥抱最近版本,快照版本倾向于积极集成开发版本,这两种都不应该用在生产环境中
gradle build --refresh-dependencies
arouter不同模块的分组不能相同,建议是路径分组与模块名相同
初始化会把带有注解的类(Activity,Fragment,服务,拦截器)信息加载到内存中(map)
Activity,Fragment先按组加载,实际调用的时候在加载组里的类
拦截器在最开始初始化
服务用到的时候初始化
没有依赖的模块间不能直接拿到类引用,但是编译时是可以的
注解处理器:
会在编译期间遍历每个类,搜索每个类中相关的注解,然后生成相关执行代码
mvp:view和presenter通过接口解耦,相互持有,代码多,事件驱动
mvvm:数据驱动,viewmodel不持有view,通过观察者模式数据变化会通知到ui,而不是直接调用更新ui的方法,通过databinding或者livedata。viewmodel和view更好的解耦,viewmodel只关心数据和逻辑,不需要通知ui更新(数据驱动)
viewmodel在屏幕旋转时不会销毁,可以保持数据,可以在fragment间数据共享
thread无法强制退出,因为要保证数据安全。调用interrupt(),将线程对象的中断标识设成true,检查终端标识Thread.interrupt()逻辑退出。
调用interrupt()时,如果线程处于被阻塞状态会抛异常
view的绘制
ViewGroup中没有定义onMeasure()方法,但他定义了measureChildren()方法
measure()主动测量,onMeasure()用来自定义测量
measureChildren(),measureChild()默认的测量子view的方法
getChildMeasureSpec()默认得到子view的MeasureSpec的方法
view.layout()先执行setFrame(),后执行onLayout()
view的宽高在setFrame()中确定,所以在onLayout()时便可以通过getWidth()获取到
没有父文件夹时不能直接创建文件
所有的人机交互代码,都得有一个死循环去持续处理人机交互代码。总不能一个main跑到底就结束了。所以Looper就出现了。Looper存在第一理由就是提供死循环。Looper.loop()后会让当前线程变成looper线程。那APP主线程总不能死循环了什么都不做吧?所以,Looper存在的第二个理由就是有M(Message)则处理,无则阻塞。(pipe/epoll机制,IO多路复用,阻塞在IO的读端,不耗费CPU资源。)
post,当post一个延时消息后系统会调用一个延时阻塞nativePollOnce(),如果再post一个无延迟消息,这是会执行唤醒操作,执行完第二个消息后再看看有没有延时消息,有的话继续延时阻塞
主线程更新ui是因为ui控件不是线程安全的
子线程绘制ui:在子线程构建looper后可以创建dialog,合理的表述:View只能被构建了ViewTree的线程操作
非静态内部类会持有外部类实例的引用
handler使用时要自定义一个静态内部类继承
view.post()在view没有调用dispatchAttachedToWindow()时runnable会被缓存起来,等到view调用dispatchAttachedToWindow()时会赋值attachInfo 并且将缓存的runnable交给handler执行,因为创建activity本身就是消息队列的一个任务,所以那些消息会加到该任务之后,可确保绘制完成
如果attachInfo 不为空则直接交给handler处理
每个view的attachInfo 共用ViewRootImpl的attachInfo
ThreadLocal:ThreadLocal对象可以针对每个线程保存一个类型的数据
ThreadLocalmap:线程的一个变量,key为ThreadLocal对象,value为想保存的数据,ThreadLocalmap保存了该线程中所有的ThreadLocal保存的数据
ThreadLocal存数据:获取当前线程的ThreadLocalmap,key为ThreadLocal对象,value为想保存的数据,存到ThreadLocalmap中
线程和ThreadLocal对象是多对多的关系,每个线程里都会有一个ThreadLocalmap,是threadlocal的静态内部类,本质是一个数组,元素类型为entry,用来保存同一线程不同的ThreadLocal对象和value。
ThreadLocalmap 数据保存的下标通过ThreadLocal对象的hashcode和数组长度-1取与来确定,最后会把ThreadLocal对象和value都保存起来
startactivity调用到ams中的startactivity,ams会判断应用进程是否存在,通过判断ApplicationThread是否为null,不存在则通过socket通知Zygote fork新的进程,在创建新进程时会通过反射调用ActivityThread 的 main 方法,main方法会初始化looper和ActivityThread ,ActivityThread.attch(thread)会把ApplicationThread关联到ams中,然后ams会通过ApplicationThread发送创建application和根activity的消息,完成启动。
如果进程存在,ams会直接调用准备创建activity的方法,然后通过ApplicationThread发送创建activity的消息,完成启动。
ApplicationThread处于binder线程
普通启动:client(startActivity) -> ams -> client
根启动:client -> ams -> Zygote -> client(ActivityThread.main()) -> new ApplicationThread() -> attach applicationThread -> ams -> client
onWindowFocusChanged方法表示首帧绘制完成,此时Activity可交互
window的实现类是phonewindow,phonewindow在activity.attch()中初始化,并且关联WindowManagerImpl,DecorView是phonewindow的内部类
ViewRoot实现类是ViewRootImpl,所有View的绘制以及事件分发等交互都是通过它来执行或传递的
setContentView():初始化DecorView,并把layout加到DecorView上
onResume():将DecorView添加到window中,初始化了ViewRootImpl,并将DecorView添加到ViewRootImpl中
WindowManagerGlobal是WindowManagerImpl的具体实现,是全局单例
ViewRootImpl 绘制 View是异步的,需要通过handler。是WindowManagerGlobal 内部实现中重要的组成部分
1.View树的根并管理View树
2.触发View的测量、布局和绘制
3.输入事件的中转站
4.管理Surface
5.负责与WMS进行进程间通信
IBinder的主要API是transact(),与它对应另一方法是Binder.onTransact()。第一个方法使你可以向远端的IBinder对象发送发出调用,第二个方法使你自己的远程对象能够响应接收到的调用。IBinder的API都是同步执行的,比如transact()直到对方的Binder.onTransact()方法调用完成后才返回。
事件分发机制:
如果down事件被拦截,那么其他的事件都会直接传递给该view
如果move事件被viewgroup拦截,该事件将会被系统变成一个CANCEL事件 & 传递给之前处理该事件的子View,该事件不会再传递给ViewGroup 的onTouchEvent(),只有再到来的move事件才会传递到ViewGroup的onTouchEvent()
接收了ACTION_DOWN事件的函数不一定能收到后续事件(可能会被viewgroup拦截)
onInterceptTouchEvent()该方法一旦返回一次true,就再也不会被调用。
若对象(Activity、ViewGroup、View)的onTouchEvent()处理了事件(返回true),那么ACTION_MOVE、ACTION_UP的事件从上往下传到该View后就不再往下传递,而是直接传给自己的onTouchEvent()& 结束本次事件传递过程。
如果消费了ACTION_DOWN,move和up事件才可以收到。如果事件无人消费,move和up事件只经过activity
滑出子View范围不会触发ACTION_CANCEL,一般情况下就是当子View的事件处理权从有到无时会触发
ACTION_OUTSIDE通常是用在dialog中,点击dialog之外的区域
requestDisallowInterceptTouchEvent(boolean b)是否禁用veiwgroup事件拦截功能
外部拦截法,父容器决定事件走向:重写onInterceptTouchEvent方法,根据条件拦截move事件
内部拦截法,view决定事件走向:重写子view 的dispatchTouchEvent方法或设置setOnTouchListener,down的时候禁用父容器拦截,move的时候根据条件拦截,同时要确保父容器不对down事件拦截,重写父容器onInterceptTouchEvent,down事件返回false
父容器可以根据情况拦截事件,子view也可以根据情况选择是否允许父容器拦截
事件分发重写dispatchTouchEvent,事件处理重写onTouchEvent
如果view消费了down没有消费move,该事件也不会传递到父容器中,所以只能用普通的拦截法
wait()线程进入等待状态
notify()线程进入就绪状态,不会释放锁
profiler
Retained Size 一个对象持有其他对象的大小
edpth 是从 GC Root 到达这个实例的最短路径,一个对象离GC Root越近就越容易被保存下来
数据库更新需考虑修改建表sql带来的影响,更新方法如果报错会导致升级失败
fragment生命周期
onAttach(),onCreate(),onCreateView(),onActivityCreate(),onStart(),onResume(),onPause(),onStop(),onDestoryView(),onDestory(),onDetach()
replace(),旧onPause()、onStop(),新onAttach()、onCreate()、onCreateView()、onActivityCreate()、onStart(),旧onDestroyView、onDestroy、onDetach,新onResume()
add(),旧生命周期不变
show(),hide(),生命周期不变
setUserVisibleHint(isVisibleToUser: Boolean)方法在onAttach之前调用,每次切换tab时也会调用
FragmentTransaction#setMaxLifecycle(Fragment, Lifecycle.State)方法可以精确控制Fragment生命周期的状态,如果Fragment的生命周期状态小于被设置的最大生命周期,则当前Fragment的生命周期会执行到被设置的最大生命周期,反之,如果Fragment的生命周期状态大于被设置的最大生命周期,那么则会回退到被设置的最大生命周期。
ViewPager2默认没有预加载,并且在fragment切换时会调用onPause和onResume,开启预加载后fragment只会执行到onstart,默认有懒加载的处理
Handler
同步屏障:为handler消息机制提供了一种优先级策略,让异步消息的优先级高于同步消息。MessageQueue.postSyncBarrie(),该方法会往MessageQueue中插入一条同步屏障message,没有给Message赋值target属性,且插入到Message队列头部。MessageQueue在获取下一个Message的时候,如果碰到了同步屏障,那么不会取出这个同步屏障,而是会遍历后续的Message,找到第一个异步消息取出并返回。这里跳过了所有的同步消息,直接执行异步消息。同步屏障不会自动移除,使用完成之后需要手动进行移除。Android 系统中更新UI就是使用同步屏障,同步屏障机制的作用,是让这个绘制消息得以越过其他的消息,优先被执行。
同步消息:我们一般发送的消息
异步消息:mAsynchronous = true ,target = null
屏幕刷新机制
CPU负责计算帧数据,把计算好的数据交给GPU,GPU会对图形数据进行渲染,渲染好后放到buffer(图像缓冲区)里存起来,然后Display(屏幕或显示器)负责把buffer里的数据呈现到屏幕上
帧率:表示GPU在1s中内可以渲染多少帧到buffer中
屏幕刷新率:屏幕在1s内去buffer中取数据的次数
画面撕裂:Display在显示的过程中,buffer内数据被CPU/GPU修改
双缓存:用来解决画面撕裂,显示器刷新一块缓存,gpu渲染另一块缓存,渲染完成后才会交换,所以可以保证画面是完整的
vsync:垂直同步,当屏幕渲染完一帧数据后,即将开始渲染下一帧之前,发出的一个同步信号,然后cpu会交换缓存,cpu计算下一帧数据,屏幕渲染新数据
View 的 requestLayout 会调到ViewRootImpl 的 requestLayout方法,然后通过 scheduleTraversals 方法向Choreographer 提交一个绘制任务,然后再通过DisplayEventReceiver向底层请求vsync信号,当vsync信号来的时候,会通过JNI回调回来,通过Handler往主线程消息队列post一个异步任务,最终是ViewRootImpl去执行那个绘制任务,调用performTraversals方法,里面是View的三个方法的回调。
卡顿原因:一个是主线程有其它耗时操作,导致doFrame没有机会在vsync信号发出之后16毫秒内调用;还有一个就是当前doFrame方法耗时,绘制太久,下一个vsync信号来的时候这一帧还没画完,造成掉帧
卡顿排查:在handler执行前后可以自定义输出log,可以用来检测handler执行时间,在开始log时开启新线程采集堆栈信息,在结束log时结束采集堆栈信息(在其他线程可以采集主线程的堆栈信息,在主线程无法采集)
RecyclerView
recyclerview动画原理
动画之前会先执行一次pre-layout,将不可见的表项 3 也加载到布局中,形成一张布局快照(1、2、3)。再为动画后的表项执行一次post-layout,同样形成一张布局快照(1、3)。比对两张快照中表项 3 的位置,就知道它该如何做动画了
为了获得两张快照,就得布局两次,分别是预布局和后布局(布局即是往列表中填充表项),
为了让两次布局互不影响,就不得不在每次布局前先清除上一次布局的内容(就好比先清除画布,重新作画),
但是两次布局中所需的某些表项大概率是一摸一样的,若在清除画布时,把表项的所有信息都一并清除,那重新作画时就会花费更多时间(重新创建 ViewHolder 并绑定数据),
RecyclerView 采取了用空间换时间的做法:在清除画布时把表项缓存在 scrap 结构中,以便在填充表项可以命中缓存,以缩短填充表项耗时。
RecyclerVeiw的四级缓存
Recycler有4个层次用于缓存ViewHolder对象,优先级从高到低依次为Scrap(分为mAttachedScrap和mChangedScrap)、ArrayList mCachedViews、ViewCacheExtension mViewCacheExtension、RecycledViewPool mRecyclerPool
mAttachedScrap:用于布局过程中屏幕可见表项的回收和复用,用于预布局和局部刷新,环缓存大小没有限制。mChangedScrap:存放可见范围内并且数据发生了变化的 ViewHolder,从这里复用的 ViewHolder 需要重新绑定数据。mCachedViews是离屏缓存,用于缓存指定位置的 ViewHolder ,只有“列表回滚”这一种场景(刚滚出屏幕的表项再次进入屏幕),才有可能命中该缓存。该缓存存放在默认大小为 2 的ArrayList中(大小可以设置)。RecycledViewPool对 ViewHolder 按viewType分类存储(通过SparseArray),同类 ViewHolder 存储在默认大小为5的ArrayList中(大小可以设置)。- 从
mRecyclerPool中复用的 ViewHolder 需要重新绑定数据,从mAttachedScrap和mCachedViews中复用的 ViewHolder 不需要重新创建也不需要重新绑定数据。 - 从
mRecyclerPool中复用的ViewHolder ,只能复用于viewType相同的表项,从mAttachedScrap和mCachedViews中复用的 ViewHolder ,只能复用于指定位置的表项。 - notifyDataSetChanged()会将所有表项无效化,先回收现存表项到缓存池,再重新填充它们
ANR
- 输入事件:5S
- 广播:前台 10S, 后台 60S
- 服务:前台 20S, 后台 200S
- ContentProvider:10S
/data/anr 目录下trace文件
常见原因:
应用层:主线程耗时操作,主线程被锁阻塞,内存紧张
系统层:cpu被抢占,内存不足,系统服务超时
日志正常考虑是系统原因或者日志没有抓取到
混淆
- R8模式下的混淆代码行号需对照mapping.txt查找实际行号
- ProGuard模式下使用sdk中的retrace工具,指定mapping.text后显示
gradle
task会默认支持全部增量构建(有变动全部构建),部分增量构建(有变动只构建变动的部分)需要主动处理
task中action在执行阶段运行,创建task时的闭包在配置阶段执行
Groovy的闭包调用:
test "123"
test("123")
test.call("123")
函数调用:
test "123"
test("123")
插件拓展通常用来配置一些属性(android{})
AAC
ViewModel通过ViewModelStore保存,内部为hashmap
ViewModel会在onRetainNonConfigurationInstance()(在Activity因配置改变而正要销毁时调用)时保存在ActivityThrtead的map<IBinder, ActivityClientRecord>里的ActivityClientRecord.lastNonConfigurationInstances的ViewModelStore中,mLastNonConfigurationInstances在activity的attch()中读取,所以可以保存ViewModel
1、onSaveInstanceState 用Bundle存储数据便于跨进程传递,而ViewModel 是Object存储数据,不需要跨进程,因此它没有大小限制。
2、onSaveInstanceState 在onStop 之后调用,比较频繁。而ViewModel 存储数据是onDestroy 之后。
3、onSaveInstanceState 可以选择是否持久化数据到文件里(该功能由ATM 实现,存储到xml里),而ViewModel 没有这功能。
livedata,onstop后暂停接收数据,onstart后会接收最新的一个数据
- postValue(xx)每次调用时将数据存储在mPendingData 变量里,然后post一个runnable切换到主线程后将mPendingData重置为默认object并setValue(),若mPendingData != 默认object则说明runnable还没有被执行,那么将不会再执行新的Runnable,因此后面的数据会覆盖前面的数据。LiveData 确保UI 能够拿到最新的数据,而此过程中的数据变化过程可能会丢失。解决方法,重写postValue(xx)使用一个全局handler发送runnable,最后执行setValue()
- 粘性事件:数据变更发生后,才注册的观察者,此时观察者还能收到变更通知。
原因:通过比对LiveData 当前数据版本与观察者的数据版本,若是发现LiveData 当前数据版本更大说明是之前没有通知过观察者,因此需要通知,反之则不通知。
解决方法:添加观察者时使用反射获取livedata的数据版本,如果不为-1则忽略第一次数据变更,如果为-1则正常更新 - 实现可以感应生命周期但是不丢失数据的livedata
解决方法:重写observe,新建一个实现Observe和LifecycleObserver接口的对象,添加mOwner!!.lifecycle.addObserver(this)
mLiveData!!.observeForever(this),重写onChanged(在生命周期结束前的任何时候都会调用),当isActive时直接onChanged,反之将消息存放到一个集合中,等onStart时将集合中的消息依次onChanged,然后清空集合
Lifecycle:lifecycle添加观察者,观察者为LifecycleObserver实现对象,然后通过注解方法观察
lifecycle无需取消观察不会造成内存泄漏,因为匿名内部类被被观察者自己持有
分区存储
Android10之后应用程序只能通过filepath和MeidaStore访问应用私有目录和媒体共享目录
Storage Access Framework可以访问任何文件无需权限
MeidaStore读写自己创建的文件不需要权限,访问其他媒体文件需要权限,不能更新其他媒体文件,且不能访问其他文档类型文件
适配可以在Android10开启兼容模式转移文件,一个targetSdk最少有一年的使用时间