文章目录
- Activity生命周期
- Android中Fragment什么时候使用
- Service的启动
- 类锁和对象锁会冲突吗
- 线程
- View的绘制流程
- Android中进程保活方法
- invalidate和requestLyout的区别
- Bitmap内存优化方式
- Handler机制
- 获取Message的方式
- 多个Handler发送消息时消息如何保证不混乱
- 子线程和主线程是如何切换的
- 如何在子线程中创建Handler
- 主线中为什么可以直接使用Handler呢
- Handler机制图解总结:
- Handler中的消息定时发送是如何实现的
- RecyclerView回收复用机制
- 调用notifyDataSetChanged之后的过程
- 如何停止一个正在运行的线程?
- 使用getExtraLayoutSpace为LayoutManager设置更多的预留空间
- 避免在onCreateViewHolder和onBindViewHolder中创建过多对象
- 调整RecyclerView的缓存
- Adapter中监听View移出和移入
- 死锁
- 生产者和消费者模型
- 自旋锁
- HashMap介绍
- 一个类的实例从new开始的过程
- java实现线程安全的方式
- uses-sdk中minSdkVersion,targetSdkVersion,maxSdKVersion
Activity生命周期
onCreate:表示Activity正在被创建,这个方法中可以做一些初始化工作
onRestart:表示Activity正在重新启动,一般情况下当Activity从不可见变为可见的时候,该方法就会调用
onStart:表示Activity正在被启动,这个时候Activity已经是可见的了,但是还没出现在前台,没办法与用户交互。也可以理解为Activity已经显示出来了,但是用户还看不到
onResume:表示Activity已经可见了,并且出现在前台开始活动了
onPause:表示Activity正在停止,onPause执行完以后,新的Activity的onResume才开始执行
onStop:表示Activity即将停止
onDestory:表示Activity即将被销毁
生命周期说明:
- 当用户打开新的activity的时候,如果新的Activity主题是透明的话,那么当前的Activity是不会走onStop。
- 当用户通过home按键重新进入原来的activity时,如果之前是将该界面放到后台,那走onRestart: onStart: onResume
- 当用户按下back按键回退时,回调如下:onPause,onStop,onDestory
- 当在一个activity中点击打开另一个activity的时候,两个activity生命周期的回调:
2021-05-22 16:27:43.079 15499-15499/com.example.recyclerviewlearn I/MainActivity : onCreate:
2021-05-22 16:27:43.081 15499-15499/com.example.recyclerviewlearn I/MainActivity : onResume:
021-05-22 16:27:45.697 15499-15499/com.example.recyclerviewlearn I/MainActivity : onPause:
2021-05-22 16:27:45.782 15499-15499/com.example.recyclerviewlearn I/SecondActivity : onCreate:
2021-05-22 16:27:45.787 15499-15499/com.example.recyclerviewlearn I/SecondActivity : onStart:
2021-05-22 16:27:45.788 15499-15499/com.example.recyclerviewlearn I/SecondActivity : onResume:
2021-05-22 16:27:46.199 15499-15499/com.example.recyclerviewlearn I/MainActivity : onStop:
当前Activity是MainActivity,待拉起的Activity是SecondActivity,两者的生命周期回调变化如上,当前Activity先onPause,被拉起的Activity执行onCreate->onStart->onResume,然后当前Activity执行onStop。
- onStart,onResume和onPause,onStop生命时机差不多,对我们来说有什么实质性的不同呢?
实际使用中两者的差别可能不大,但是这两对回调表示的时机是不同的,onStart和onStop是从Activity是否可见的角度来回调的。onResume和onPause是从Activity是否位于前台的这个角度来回调的。
Android中Fragment什么时候使用
碎片可以让界面可以更好的在平板和手机上进行展示,fragment可以将布局和代码一起封装,
例子:一款APP包含界面A和界面B,界面B为界面A的详情。你需要同时适配手机和平板,手机版的操作逻辑为A跳转到B,而平板的布局为AB同一界面,A在左边,B在右边。这个时候更好的就是将两个界面封装到Fragment中。
Service的启动
方式一:
启动服务的时候可以使用startService(Intent)方法启动该Service,
调用startService()方法生命周期函如下:
onCreate()—>onStartCommand()(onStart()方法已过时) —> 销毁:onDestory()
上面几个方法的执行都是在主线程中。
如果服务还没有创建,会走onCreate(),否则,只会调用
onStart()和onStartCommand()。
停止服务使用stopService,onDestory只会在服务销毁的时候执行,服务只能停止一次,停止服务使用。
备注:一旦服务开启跟调用者(开启者)就没有任何关系了。
开启者退出了,开启者挂了,服务还在后台长期的运行。
方式二:
使用bindService的方式启动该服务。
使用bindService方法启动Service时,生命周期如下
onCreate() —>onBind()—>onunbind()—>onDestory()
可以使用unbindService停止服务。
备注:绑定服务不会调用onstart()或者onstartcommand()方法。bind的方式开启服务,绑定服务,调用者挂了,服务也会跟着挂掉。
调用该服务的client销毁的时候,client会自动解除与Service的绑定,当然Client也可以明确调用unBindService与Service解除绑定,当没有任何client与Service绑定时,Service会自行销毁,销毁的时候会执行unBind()->onDestory方法。
多次调用bindService,服务端只会执行一次onBind方法,多个客户端共享同一个BIndler对象。
方式一和方式二混用
使用了startService和bindService两种方式启动服务时,需要使用unbindService,也需要使用stopSevice,这样才能将服务停止,只要有一种启动方式还存在,service就会继续存活。
类锁和对象锁会冲突吗
首先结论是:类锁和对象锁不会冲突
- 对象锁
Java中的对象会有一个互斥锁,synchronized方法正常返回或者抛出异常而中止,jvm会自动释放锁。 - 类锁
在代码中的方法上加了static和synchronized锁,或者synchronized(xxx.class)的代码段就称之为类锁。
类锁用来控制静态方法之间的同步,java中可能会有很多个对象,但是Class对象只有一个,所以其实类锁也只是Class对象的锁。
类锁和对象锁,一个是Class对象的锁,一个是普通对象的锁,因此两个锁是不同的,因此类锁和对象锁不会产生竞争,两者的加锁方式不会影响。
线程
什么是线程,什么是进程?
进程是具有一定独立功能的程序,是操作系统进行资源调度和分配的一个独立单元,就像每一个独立的apk基本都是运行在一个进程中,进程拥有独立的内存单元。
线程是CPU调度和分派的基本单位,进程中是靠线程在运行。
使用线程的三种方式
方式一:继承Thread类,重写run方法,然后调用start()方法。
方式二:实现Runnable接口,实现run方法,然后将Runnable对象传到Thread的构造函数中,然后start()。
方式三:实现Callable接口,call接口可以返回值,并且抛出异常,使用该方式需要FutureTask的支持,将创建好的FutureTask传递到Thread构造函数中,然后start
synchronized和ReentrantLock的区别
synchronized是关键字,ReentrantLock是类,
- ReentrantLock可以对获取的等待时间进行设置,这样就避免了死锁。
- ReentrantLock在获取锁之后一定要手动释放。
java中使用线程池
java中线程也不是越多性能越好,因为线程之间的切换也是耗时,当需要使用多个线程时,可以创建固定大小的线程池,减少创建和销毁线程对象的开销。
java中提供了Executor接口创建线程池,
ExecutorService es2 = Executors.newFixedThreadPool(3);
线程同步以及调度相关的方法
- wait():使当前线程处于阻塞状态,并释放所持有的对象的锁,直到另一个线程为当前对象调用了notify和notifyAll()
- sleep():使一个线程处于睡眠状态
- notify():唤醒一个处于等待当前对象锁的线程,由jvm确定唤醒哪个线程,与优先级无关。
- notityAll():唤醒所有等待当前对象锁的线程,该方法并不是将对象的锁给所有线程,而是让它们竞争,只有获得锁的线程才能进入就绪状态
ThreadLocal介绍
ThreadLocal是一个线程内部的数据存储类,不在多个线程间共享,这是一种线程安全的方式。
ThreadLocal给每个线程中都存储了一个该变量的副本,主要有下面三个方法:
void set(T value):设置当前线程的线程局部变量的值。
T get():获得当前线程所对应的线程局部变量的值。
void remove():删除当前线程中线程局部变量的值。
查看Thread类中的代码:
ThreadLocal.ThreadLocalMap threadLocals = null;
每个Thread类都维护了一个 threadLocals,通过currentThread()获取到当前线程,从而获取到当前线程的ThreadLocalMap,将需要放置的值放到map中,每个线程维护了一个ThreadLocalMap,该map中存储了对应的变量值,ThreadLocal的Set方法如下:
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
查看set方法,首先获取了当前线程的ThreadLocalMap对象,然后给该map中放置了对映的value,所以两个线程之间变量不会相互影响。
查看启动耗时:
本地使用命令查看:
adb shell am start -W packageName/activity
adb shell am start -W com.example.activitystart/.MainActivity
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.example.activitystart/.MainActivity }
Status: ok
Activity: com.example.activitystart/.MainActivity
ThisTime: 377
TotalTime: 377
WaitTime: 400
Complete
本地启动时长:
ThisTime
最后一个Activity启动耗时,如果关心应用有界面Activity启动耗时,参考ThisTime
TotalTime
所有Activity启动耗时,一般查看得到的TotalTime,即应用的启动时间,包括创建进程 + Application初始化 + Activity初始化到界面显示的过程。如果只关心某个应用自身启动耗时,参考TotalTime
WaitTime
AMS启动Activity的总耗时,如果关心系统启动应用耗时,参考WaitTime
参考:https://blog.csdn.net/s_nshine/article/details/105554697
代码中通过加log的方式计时:
起始时间:
冷启动:Application.attachBaseContext();
热启动:Activity.onReStart();
结束时间:
Activity.onWindowFocusChanged()
为什么onWindowFocusChanged()可以作为最佳的判断结束时间的标准呢?查看android中的注释,
/**
* Called when the current {@link Window} of the activity gains or loses
* focus. This is the best indicator of whether this activity is the entity
* with which the user actively interacts
翻译大体就是表示这是最佳的获取activity可以和用户交互的时机。
为什么不能使用onResume呢,因为Activity走完onCreate,onStart,到达onResume状态时,View还需要经过onMeasure,onLayout,onDraw三个流程才能显示出来。
2021-04-05 22:26:44.127 2109-2109/com.example.customizeview I/MainActivity: onStart:
2021-04-05 22:26:44.128 2109-2109/com.example.customizeview I/MainActivity: onResume:
2021-04-05 22:26:44.151 2109-2109/com.example.customizeview I/CircleView: onMeasure:
2021-04-05 22:26:44.160 2109-2109/com.example.customizeview I/CircleView: onMeasure:
2021-04-05 22:26:44.161 2109-2109/com.example.customizeview I/CircleView: onLayout:
View的绘制流程
绘制的起点:Android中View树绘制的起点开始于performTraversals方法,当Activity创建完毕后,ViewRoot和DecorView关联完毕之后会调用performTraversals方法开始绘制,performTraversals回依次调用(ViewGroup)performMeasure->(View)measure,(ViewGroup)performLayout->(View)layout,(ViewGroup)performDraw->(View)draw,
View的Measure
View在绘制的时候首先会measure自己的宽高,子类可以重写onMeasure方法实现自己的测量,对于ViewGroup在Measure的时候需要测量自身的大小并且还需要调用子View的Measure方法,用以测量子View的大小。
View的Layout
View在layout的时候首先会调用layout来确认自身的位置,随后可以在onLayout中确认子元素的位置。
View的Draw
View在绘制的时候回调用draw方法进行相关绘制,需要改变View自身的绘制内容时,我们可以通过重写onDraw方法来完成,
当需要在ViewGroup中绘制内容时,需setWillNotDraw()为false来关闭优化。
参考:https://blog.csdn.net/liu_12345_liu/article/details/107900143
Android中进程保活方法
若是系统应用,增加persistent属性,表明为常驻应用。在杀死后会被系统重新拉起。
invalidate和requestLyout的区别
调用invalidate不会导致onMeasure和onLayout调用,只会调用onDraw,
调用requestLayout会导致onMeasure和onLayout被调用,不一定会触发onDraw()。例如:发现边界不一致,会调用一次onDraw。
参考:https://blog.csdn.net/hxl517116279/article/details/90410345
Bitmap内存优化方式
- 使用inBitmap复用内存,减少内存的创建和销毁
- 选用大小合适的图片,可以使用inDensity和inTargetDensity进行图片的缩放,或者使用inSampleSize使用采样率降低内存占用。
- 降低性能减少内存占用,例如:非必要条件下不使用硬件加速,可以降低内存。
- 使用优化过的数据类型:SparseArray、SparseBooleanArray等。
- 使用memory profiler分析内存,查看占用,同时可以借助该工具查看内存泄漏情况,对使用的内存进行及时的释放。
Handler机制
Android中UI必须在主线程中更新,使用Handler,我们可以在子线程中更新UI。
Handler在创建的时候会使用当前线程的Looper来构建消息循环系统,如果当前线程没有Looper会报错。
handler调用send方法发送消息时,MessageQuque会将这个消息放到消息队列中,当Looper发现消息到来时,就会处理这个消息,Looper是运行在Handler创建的时候所在的线程中的,这样一来就可以将Handler中的业务切换到创建Handler所在的线程中了。
Handler中使用Looper的作用域是当前的线程,因为ThreadLocal实现了Looper在不同线程之间的隔离,线程之间互不影响。
消息队列的工作机制
MessageQueue内部是通过一个单链表的数据结构来维护消息的,因为单链表在插入和删除上比较有优势。主要有两个方法,enqueueMessage和next方法。enqueueMessage在单链表中插入了消息。next方法用于移除消息,如果消息队列中没有消息,next方法中会一直阻塞,等待有新的消息出现,next会返回这条消息,并将其从单链表中删除。
Looper的工作原理
Looper在Android中扮演着消息循环的角色,具体的来说就是一直从MessageQueue中查询是否有新的消息,有新消息的话就会处理,否则就阻塞在那里。
Looper在自己的构造函数中会创建一个MessageQueue
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}
当handler所在的线程没有Looper的时候,就会报错,为一个线程创建Looper的方法如下:
new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();//Looper初始化
//Handler初始化 需要注意, Handler初始化传入Looper对象是子线程中缓存的Looper对象
mHandler = new Handler(Looper.myLooper());
Looper.loop();//死循环
//注意: Looper.loop()之后的位置代码在Looper退出之前不会执行,(并非永远不执行)
}
}).start();
查看Looper.prepare()的代码:
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}
prepare是使用ThreadLoadl为当前线程创建一个Looper,Looper.loop方法如下,主要是一个死循环,只有Messagequeue.next返回为null的时候,即Looper的quit方法被调用时,才会结束退出:
/**
* Run the message queue in this thread. Be sure to call
* {@link #quit()} to end the loop.
*/
public static void loop() {
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
final MessageQueue queue = me.mQueue;
for (;;) {
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}
...
msg.target.dispatchMessage(msg);
...
}
}
loop函数中会调用,MessageQueue.next来取消息,当没有消息时MessageQueue.next会一直阻塞,拿到新的消息后,Looper调用msg.target.dispatchMessage(msg),这里的msg.target是发送消息的Handler对象,但是Handler的dispatchMessage是在创建Handler所在的Looper中执行的,这样就成功将代码逻辑切换到指定线程了(因为在发送消息的时候会调用到enqueueMessage,enqueueMessage发送的时候使用的是Looper中的queue,这样就将消息发送到了对应线程中,对应线程中的Looper就可以处理了)。
Looper也是可以退出的,在不需要使用的时候可以调用quit和quitSafely来退出Looper,quit调用后会直接退出,quitSafely调用后会设置一个标记,等待消息队列中的消息全部处理完毕后才安全退出,在子线程中,如果手动为其创建了Looper,则应该在所有的事情做完后退出Looper,否则子线程会一直处于等待的状态。
Handler的工作原理
Handler的工作分为接收消息和发送消息两个过程,发送消息的时候post最终也是调用的send方法,调用send方法最终调用到了enqueueMessage:
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
long uptimeMillis) {
msg.target = this;
msg.workSourceUid = ThreadLocalWorkSource.getUid();
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
Handler发送消息会将消息添加到MessageQueue中,MessageQueue的next方法会读取消息,最终消息由Looper交给Handler去处理,Handler处理消息的过程如下:
/**
* Handle system messages here.
*/
public void dispatchMessage(@NonNull Message msg) {
if (msg.callback != null) {
handleCallback(msg);
} else {
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
}
}
Handler在处理消息的时候,首先判断当前消息是否有自己的callback,有的话就回调整个CallBack。
当没有callback的时候,首先检查mCallback是否为空,不为空的话就掉用,这个Callback是创建Handler的时候传入的,这样就可以不用派生子类实现handlerMessage()方法,callback接口如下:
/**
* Callback interface you can use when instantiating a Handler to avoid
* having to implement your own subclass of Handler.
*/
public interface Callback {
/**
* @param msg A {@link android.os.Message Message} object
* @return True if no further handling is desired
*/
boolean handleMessage(@NonNull Message msg);
}
当mCallback为空的时候,就调用Handler自身实现的handleMessage方法。
那么,callback,msg和runnable的执行优先级是什么呢?
查看代码,在直接postRunnable的时候,也是构建了一个msg,将runnable放入到msg的callback中。
public final boolean post(Runnable r)
{
return sendMessageDelayed(getPostMessage(r), 0);
}
private static Message getPostMessage(Runnable r) {
Message m = Message.obtain();
m.callback = r;
return m;
}
所以msg和runnable的优先级相同。查看上面的dispatchMessage方法,msg的优先级大于callback,mCalback的优先级大于handleMessage()
if (mCallback != null) {
if (mCallback.handleMessage(msg)) {
return;
}
}
handleMessage(msg);
获取Message的方式
Android中获取Message实例的方法有很多,可以直接创建,同时也可以使用Message.obtain()和Handler.obtatinMessage()方法来得到一个Message对象。
这样我们就可以对对象进行复用,两者最终都是调用了Message.obtain()方法:
public static Message obtain() {
synchronized (sPoolSync) {
if (sPool != null) {
Message m = sPool;
sPool = m.next;
m.next = null;
m.flags = 0; // clear in-use flag
sPoolSize--;
return m;
}
}
return new Message();
}
多个Handler发送消息时消息如何保证不混乱
Handler中无论哪种方式发送消息,最终都会调用到enqueueMessage,代码如下:
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
long uptimeMillis) {
msg.target = this;
msg.workSourceUid = ThreadLocalWorkSource.getUid();
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
在将新的消息入队的时候会将当前对象的target设置为当前Handler,同时queue也是Looper中的成员变量,也就是当前线程对应的queue,
mQueue = mLooper.mQueue;
消息入队到,在处理消息的时候,调用
msg.target.dispatchMessage(msg);
调用的就是发送消息的handler的处理消息的接口,这样消息就由对应的Handler处理了。
子线程和主线程是如何切换的
子线程用Handler发送消息,发送的消息会被发送到对应Handler也就是主线程的MessageQueue中,也就会被与主线程相关的Looper处理,Handler与哪个Looper的线程相关,Looper对应就运行在与之相关的线程中,所以消息就会在对应的线程中处理。
如何在子线程中创建Handler
参减:实现一个子线程的Handler中的实现一个子线程的Handler
主线中为什么可以直接使用Handler呢
因为ActivityThread的Main方法当中已经调用了Looper.prepareMainLooper();创建了主线程的Looper,因此在主线程中我们可以直接使用Handler。
Handler机制图解总结:
参考:https://www.jianshu.com/p/592fb6bb69fa
Handler中的消息定时发送是如何实现的
handler中postDelay发送延迟消息的时候:
public final boolean sendMessageDelayed(Message msg, long delayMillis)
{
if (delayMillis < 0) {
delayMillis = 0;
}
return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
MessageQueue queue = mQueue;
if (queue == null) {
RuntimeException e = new RuntimeException(
this + " sendMessageAtTime() called with no mQueue");
Log.w("Looper", e.getMessage(), e);
return false;
}
return enqueueMessage(queue, msg, uptimeMillis);
}
MessageQueue在入队的时候,会将延迟时间长的消息放置在消息队列尾部,即根据when的大小,
when的数据越大在链表中的位置越靠后。
在next方法中,会检测当前时间与msg待执行的时间when的关系,如果当前时间小于when的话,则通过nativePollOnce设置一个阻塞,进行延迟唤醒。否则,将该消息消耗掉。
Message next() {
// Return here if the message loop has already quit and been disposed.
// This can happen if the application tries to restart a looper after quit
// which is not supported.
final long ptr = mPtr;
if (ptr == 0) {
return null;
}
int pendingIdleHandlerCount = -1; // -1 only during first iteration
int nextPollTimeoutMillis = 0;
for (;;) {
if (nextPollTimeoutMillis != 0) {
Binder.flushPendingCommands();
}
// 阻塞延迟,该方法最终通过linux中的epoll机制实现,调用epoll_wait
nativePollOnce(ptr, nextPollTimeoutMillis);
synchronized (this) {
// Try to retrieve the next message. Return if found.
final long now = SystemClock.uptimeMillis();
Message prevMsg = null;
Message msg = mMessages;
if (msg != null && msg.target == null) {
// Stalled by a barrier. Find the next asynchronous message in the queue.
do {
prevMsg = msg;
msg = msg.next;
} while (msg != null && !msg.isAsynchronous());
}
如果在等待超时的过程中,又有消息发送过来的话,
// Inserted within the middle of the queue. Usually we don't have to wake
// up the event queue unless there is a barrier at the head of the queue
// and the message is the earliest asynchronous message in the queue.
needWake = mBlocked && p.target == null && msg.isAsynchronous();
Message prev;
for (;;) {
prev = p;
p = p.next;
if (p == null || when < p.when) {
break;
}
if (needWake && p.isAsynchronous()) {
needWake = false;
}
}
msg.next = p; // invariant: p == prev.next
prev.next = msg;
}
// We can assume mPtr != 0 because mQuitting is false.
if (needWake) {
nativeWake(mPtr);
}
在入队的时候会判断,如果当前消息添加在队列首部的话,且msg.when大于当前时间,即执行时间比当前最早消息执行的时间早,会调用nativeWake方法唤醒阻塞的线程,发送需要执行的消息。
RecyclerView回收复用机制
RecyclerView在回收和复用的时候,当旧的View移出屏幕的时候,新的View移入屏幕,这两者是谁先工作的呢?
在向下滑动的过程中,是新item的复用机制先进行工作,然后item的回收机制才进行工作。
public View getViewForPosition(int position) {
return getViewForPosition(position, false);
}
getViewForPosition方法是复用机制的入口,Recycler对外部提供这个api用以获取需要的View。
Recycler内部实现的时候有几个结构体,用来缓存ViewHolder:
比较重要的几个集合:
- mAttachedScrap: 主要用来缓存在屏幕上可见的ViewHolder。
- mChangedScrap:主要用在Item发生变化时回调,且只在预布局阶段使用。
- mCachedViews:这个就重要得多了,滑动过程中的回收和复用都是先处理的这个 List,这个集合里存的 ViewHolder 的原本数据信息都在,所以可以直接添加到 RecyclerView 中显示,不需要再次重新 onBindViewHolder()。
- mRecyclerPool:这个也很重要,但存在这里的 ViewHolder 的数据信息会被重置掉,相当于 ViewHolder 是一个重创新建的一样,所以需要重新调用 onBindViewHolder 来绑定数据
mAttachedScrap和mCachedView不会执行onBind,因为数据还是原来的数据
RecyclerView的复用主要在
ViewHolder tryGetViewHolderForPositionByDeadline(int position,
boolean dryRun, long deadlineNs) {
该该方法中,首先判断判断是否是isPreLayout(),如果是的话就从mChangedScrap中获取ViewHolder。
if (mState.isPreLayout()) {
holder = getChangedScrapViewForPosition(position);
fromScrapOrHiddenOrCache = holder != null;
}
接下来下面的方法中获取数据:
// 1) Find by position from scrap/hidden list/cache
if (holder == null) {
holder = getScrapOrHiddenOrCachedHolderForPosition(position, dryRun);
...
getScrapOrHiddenOrCachedHolderForPosition函数在实现的时候,主要从scrap view,hidden children,cache三个地方获取ViewHolder。
Returns a view for the position either from attach scrap, hidden children, or cache.
ViewHolder getScrapOrHiddenOrCachedHolderForPosition(int position, boolean dryRun) {
final int scrapCount = mAttachedScrap.size();
// Try first for an exact, non-invalid match from scrap.
...
final ViewHolder holder = mAttachedScrap.get(i);
...
View view = mChildHelper.findHiddenNonRemovedView(position);
...
// Search in our first-level recycled view cache.
final int cacheSize = mCachedViews.size();
for (int i = 0; i < cacheSize; i++) {
final ViewHolder holder = mCachedViews.get(i);
...
return null;
}
其中,mAttachedScrap可以理解为屏幕上需要显示的ViewHolder暂存的地方,这里的ViewHolder不会发生回收和复用。findHiddenNonRemovedView这个看名字和复用与否没有关系。mCachedViews的默认大小是2,mCachedViews里存放的ViewHolder的信息都保存着,所以只有原来位置的卡片可以复用这个ViewHolder。
接着会在getScrapOrCachedViewForId()方法中根据id进行查找,
// 2) Find from scrap/cache via stable ids, if exists
if (mAdapter.hasStableIds()) {
holder = getScrapOrCachedViewForId(mAdapter.getItemId(offsetPosition),
type, dryRun);
一般情况下在Adapter中不会重写该属性,重写了的话这里会再根据id去查找一遍,重写方法参见:hasStableIds,重写该接口可以做到优化优化notifyDataSetChanged的时间。
接下来会从ViewCacheExtension中寻找复用的View,这个可以由用户自己拓展:
if (holder == null && mViewCacheExtension != null) {
// We are NOT sending the offsetPosition because LayoutManager does not
// know it.
final View view = mViewCacheExtension
.getViewForPositionAndType(this, position, type);
当前面的过程中都没有找到的时候,接下来会到getRecycledViewPool中寻找:
if (holder == null) { // fallback to pool
if (DEBUG) {
Log.d(TAG, "tryGetViewHolderForPositionByDeadline("
+ position + ") fetching from shared pool");
}
holder = getRecycledViewPool().getRecycledView(type);
if (holder != null) {
holder.resetInternal();
if (FORCE_INVALIDATE_DISPLAY_LIST) {
invalidateDisplayListInt(holder);
}
}
}
在RecyclerViewPool中,会根据viewType去复用对应的ViewHolder,RecyclerViewPool中每个类型的view缓存默认大小是5
public ViewHolder getRecycledView(int viewType) {
final ScrapData scrapData = mScrap.get(viewType);
if (scrapData != null && !scrapData.mScrapHeap.isEmpty()) {
final ArrayList<ViewHolder> scrapHeap = scrapData.mScrapHeap;
for (int i = scrapHeap.size() - 1; i >= 0; i--) {
if (!scrapHeap.get(i).isAttachedToTransitionOverlay()) {
return scrapHeap.remove(i);
}
}
}
return null;
}
找到了对应Type的ViewHolder的话,就将最后一个ViewHolder拿出来复用,复用之前先将View上面的信息重置,holder.resetInternal(),因此从这里拿出来的ViewHolder需要重新执行onBindViewHolder。如果在RecyclerViewPool中还是没有找到那么会去执行createViewHolder去创建新的ViewHolder。
holder = mAdapter.createViewHolder(RecyclerView.this, type);
创建了ViewHolder之后,会调用tryBindViewHolderByDeadline去执行onBindViewHolder
final int offsetPosition = mAdapterHelper.findPositionOffset(position);
bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
至此,tryGetViewHolderForPositionByDeadline方法的执行就基本结束了,该方法是RecyclerView复用机制的主要实现。
回收机制
当RecyclerView在滑动的时候,回收通过recycleView来执行:
public void recycleView(@NonNull View view) {
// This public recycle method tries to make view recycle-able since layout manager
// intended to recycle this view (e.g. even if it is in scrap or change cache)
ViewHolder holder = getChildViewHolderInt(view);
if (holder.isTmpDetached()) {
removeDetachedView(view, false);
}
if (holder.isScrap()) {
holder.unScrap();
} else if (holder.wasReturnedFromScrap()) {
holder.clearReturnedFromScrapFlag();
}
recycleViewHolderInternal(holder);
LinearLayoutManager的scrollVerticallyBy()执行的时候会调用fill()方法处理View的回收和复用,最终会调用recyclerView->recycleViewHolderInternal来进行回收复用工作,。
void recycleViewHolderInternal(ViewHolder holder) {
boolean cached = false;
boolean recycled = false;
...
if (forceRecycle || holder.isRecyclable()) {
if (mViewCacheMax > 0
&& !holder.hasAnyOfTheFlags(ViewHolder.FLAG_INVALID
| ViewHolder.FLAG_REMOVED
| ViewHolder.FLAG_UPDATE
| ViewHolder.FLAG_ADAPTER_POSITION_UNKNOWN)) {
// Retire oldest cached view
int cachedViewSize = mCachedViews.size();
// 如果mCache满了,把index为0的ViewHolder放置到RecyclerViewPool中
if (cachedViewSize >= mViewCacheMax && cachedViewSize > 0) {
recycleCachedViewAt(0);
cachedViewSize--;
}
...
// 将新的view放置到mCacheScrap中
mCachedViews.add(targetCacheIndex, holder);
cached = true;
...
if (!cached) {
// 将View放置到RecyclerViewPool中等待复用
addViewHolderToRecycledViewPool(holder, true);
recycled = true;
}
RecyclerView在回收的时候,首先会将ViewHolder放置到mCachedView中,如果mCachedView满了的话就再放置到RecyclerViewPool中,然后将新的ViewHolder放置到RecyclerViewPool中。
总结:
RecyclerView 滑动场景下的回收复用涉及到的结构体两个:
mCachedViews 和 RecyclerViewPool
mCachedViews 优先级高于 RecyclerViewPool,回收时,最新的 ViewHolder 都是往 mCachedViews 里放,如果它满了,那就移出一个扔到 RecyclerViewPool 里好空出位置来缓存最新的 ViewHolder。
复用时,也是先到 mCachedViews 里找 ViewHolder,但需要各种匹配条件,概括一下就是只有原来位置的卡位可以复用存在 mCachedViews 里的 ViewHolder,如果 mCachedViews 里没有,那么才去 ViewPool 里找。同时再执行回收复用逻辑的时候,RecyclerView是先复用再回收。
参考:
RecyclerView回收复用分析
RecyclerView性能优化
调用notifyDataSetChanged之后的过程
当通过adapter.notifyDataSetChanged()通知数据变化时,如果每个Item没有稳定的id的话,RecyclerView会假设所有的item都变化,屏幕上所有的Item都会layout一遍,当RecyclerViewPool中的缓存不够的时候,会重新创建新的ViewHolder。
界面布局如下:
绿色的部分表示item,一共有10个Item,当调用notifyDataSetChanged之后,查看日志:
一共调用了5次onCreateHolder,因为RecyclerViewPool中存有5个缓存。
当设置了StableIds时,调用notifyDataSetChanged则不会执行上面的流程,只执行了bind过程,节省了资源。
// 设置stableIds未true
setHasStableIds(true);
// 重写getItemId,这里未每个item生成唯一标识
@Override
public long getItemId(int position) {
return mStringList.get(position).hashCode();
}
查看日志:
只有onBind没有onCreate的过程,节省了View创建的流程。
如何停止一个正在运行的线程?
使用this.interrupt()方法设置对应的标记,在线程的循环中使用isInterrupted()进行判断,中断的话就停止线程运行。
自己测试的时候发现。这个线程如果被Handler占用的话,调用interrupt不起作用,但是这个时候可以调用handler的quit方法。
使用getExtraLayoutSpace为LayoutManager设置更多的预留空间
启动应用之后,在屏幕可见范围内,如果只有一张卡片可见,当滚动的时 候,RecyclerView找不到可以重用的view了,它将创建一个新的,因此在滑动到第二个item的时候就会有一定的延时,但是第二个item之 后的滚动是流畅的,因为这个时候RecyclerView已经有能重用的view了。
这种情况下我们应该使用getExtraLayoutSpace去预留更多的显示空间:
LinearLayoutManager linearLayoutManager = new LinearLayoutManager(this) {
@Override
protected int getExtraLayoutSpace(RecyclerView.State state) {
return 300;
}
};
避免在onCreateViewHolder和onBindViewHolder中创建过多对象
recyclerView的onBindViewHolder方法在复用View的时候会执行,执行次数多余onCreateViewHolder的次数,因此在onBindViewHolder中尽可能的减少对象的创建。
下面的写法就会反复创建View.OnClickListener对象:
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
holder.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
//do something
}
});
}
可以将Listener的提前到创建View的时候:
private class XXXHolder extends RecyclerView.ViewHolder {
private EditText mEt;
EditHolder(View itemView) {
super(itemView);
mEt = (EditText) itemView;
mEt.setOnClickListener(mOnClickListener);
}
}
private View.OnClickListener mOnClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
//do something
}
}
调整RecyclerView的缓存
setItemViewCacheSize修改mCachedViews大小
mCachedViews默认大小是2,mCachedViews中的View在position相同的时候可以直接使用,不需要onBindViewHolder,对于可能来回滑动的场景,我们可以把mCachedViews设置大一些,减少Bind的时间。
(用空间换时间)
复用mRecyclerPool
当多个RecyclerView有相同的item布局结构时,多个RecyclerView复用一个mRecyclerPool可以减少ViewHolder的创建。
使用方式:
RecycledViewPool mPool = mRecyclerView1.getRecycledViewPool();
mRecyclerView2.setRecycledViewPool(mPool);
mRecyclerView3.setRecycledViewPool(mPool);
注意:
(1)在复用的时候保证view type不会冲突,或者需要确保是多个RecyclerView是同一个Adapter。
(2)可以通过mRecyclerView.getRecycledViewPool().setMaxRecycledViews()设置mRecyclerPool中该类型的ViewHolder的缓存数量。
(3)可以在设置复用的时候建议设置layout.setRecycleChildrenOnDetach(true);此属性是用来告诉LayoutManager从RecyclerView分离时,是否要回收所有的ite
Adapter中监听View移出和移入
当需要根据View移出,或者移入屏幕做一些事情的时候,可以重写adapter中的onViewDetachedFromWindow和onViewAttachedToWindow两个方法,
@Override
public void onViewDetachedFromWindow(@NonNull MyViewHolder holder) {
super.onViewDetachedFromWindow(holder);
TextView textView = holder.itemView.findViewById(R.id.textView);
Log.i(TAG, "onViewDetachedFromWindow: " + textView.getText());
}
@Override
public void onViewAttachedToWindow(@NonNull MyViewHolder holder) {
super.onViewAttachedToWindow(holder);
TextView textView = holder.itemView.findViewById(R.id.textView);
Log.i(TAG, "onViewAttachedToWindow: " + textView.getText());
}
查看注解:
/**
* Called when a view created by this adapter has been attached to a window.
*
* <p>This can be used as a reasonable signal that the view is about to be seen
* by the user. If the adapter previously freed any resources in
* {@link #onViewDetachedFromWindow(RecyclerView.ViewHolder) onViewDetachedFromWindow}
* those resources should be restored here.</p>
*
* @param holder Holder of the view being attached
*/
public void onViewAttachedToWindow(@NonNull VH holder) {
}
onViewAttachedToWindow时,表示View被用户可见,但是当adapter在onViewDetachedFromWindow中释放了资源的话,应该在onViewAttachedToWindow方法中重新加载。
/**
* Called when a view created by this adapter has been detached from its window.
*
* <p>Becoming detached from the window is not necessarily a permanent condition;
* the consumer of an Adapter's views may choose to cache views offscreen while they
* are not visible, attaching and detaching them as appropriate.</p>
*
* @param holder Holder of the view being detached
*/
public void onViewDetachedFromWindow(@NonNull VH holder) {
}
当View从Window上分离的时候调用,可以在这里执行资源释放的相关动作。
自己加日志进行调试发现,View会在移出屏幕时调用detach,移入屏幕时调用attach。
死锁
死锁是因为多个线程因为资源竞争造成的一种互相等待的现象。例如,在某一个计算机系统中只有一台打印机和一台输入 设备,进程P1正占用输入设备,同时又提出使用打印机的请求,但此时打印机正被进程P2 所占用,而P2在未释放打印机之前,又提出请求使用正被P1占用着的输入设备。这样两个进程相互无休止地等待下去,均无法继续执行。
死锁产生的i原因:
1.资源在竞争的过程中分配不当。
2.运行过程中,线程请求和释放资源的顺序不当。
产生死锁的四个必要条件:
下面进程也可以替换为线程
- 互斥条件:一个资源某个时间段内只能被一个进程使用,其它进程如果需要该资源,则只能等待。
- 请求与保持:进程已经保持了至少一种资源,但又提出了新的资源请求,而该资源已经被其它进程占有,此时请求进程被阻塞,但自己对已经获得的资源保持不释放
- 不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能 由获得该资源的进程自己来释放(只能是主动释放)
- 循环等待条件: 若干进程间形成首尾相接循环等待资源的关系
避免死锁的方式:避免进程永久占用资源,减少代码中的循环等待。
生产者和消费者模型
实现的时候主要涉及下面两个方法:
wait和notify方法:
- wait()方法:当缓冲区满/空的时候,生产者/消费者会停止自己的运行,放弃锁,让自己处于等待队列中,让其他线程运行。
- notify():当生产者/消费者向缓冲区放入/取出一个产品时,向其他等待的线程发出可执行的通知,同时放弃锁,使自己处于等待状态。
代码:
package 并发练习;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Random;
public class ProducerConsumer {
private static final int CAPACITY = 5;
public static void main(String[] args) {
Queue<Integer> queue = new LinkedList<Integer>();
Thread producer1 = new Producer("P-1", queue, CAPACITY);
Thread producer2 = new Producer("P-2", queue, CAPACITY);
Thread consumer1 = new Consumer("C1", queue, CAPACITY);
Thread consumer2 = new Consumer("C2", queue, CAPACITY);
Thread consumer3 = new Consumer("C3", queue, CAPACITY);
producer1.start();
producer2.start();
consumer1.start();
consumer2.start();
consumer3.start();
}
public static class Producer extends Thread {
private Queue<Integer> mQueue;
private String mName;
int mMaxSize;
int i = 0;
public Producer(String name, Queue<Integer> queue, int maxSize) {
super(name);
mName = name;
mMaxSize = maxSize;
mQueue = queue;
}
@Override
public void run() {
super.run();
while (true) {
synchronized (mQueue) {
while (mQueue.size() == mMaxSize) {
System.out.println("Queue is full" + mName + " to wait.");
try {
mQueue.wait();
System.out.println("Producer " + mName + " wait end");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// synchronized执行的时候会先将工作你七寸清空
// 然后获得主存中最新的这个值
System.out.println("" + mName + " Producer product " + i);
mQueue.offer(i++);
mQueue.notifyAll();
try {
Thread.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
public static class Consumer extends Thread {
private Queue<Integer> mQueue;
String mName;
int mMaxSize;
public Consumer(String name, Queue<Integer> queue, int maxSize) {
super(name);
mName = name;
mQueue = queue;
mMaxSize = maxSize;
}
@Override
public void run() {
super.run();
while (true) {
synchronized (mQueue) {
while (mQueue.isEmpty()) {
System.out.println("queue is empty" + mName + " go to wait");
try {
mQueue.wait();
System.out.println("Consumer " + mName + " wait end");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("Consumer value " + mQueue.poll());
mQueue.notifyAll();
try {
Thread.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
自旋锁
自旋锁的定义:当一个线程尝试去获取某一把锁的时候,如果这个锁已经被另外一个线程占有了,那么此线程就无法获取这把锁,该线程会等待,间隔一段时间后再次尝试获取。这种采用循环加锁,等待锁释放的机制就称为自旋锁(spinlock)
自旋锁的优点:在线程竞争不激烈的时候,自旋锁是需要等待前面一个获的锁的线程释放锁后就可以在很短的时间内获取到锁从而继续执行,避免了直接将线程阻塞再去重新等待CPU调度所浪费的两次上下文切换的系统开销,这可以极大的提升系统的性能。
在线程竞争很激烈的时候,自旋锁就显得有一点笨拙了。因为自旋锁在获取到锁资源之前CPU是一直在做无用功,同时大量的线程去竞争一个锁资源,会导致获取锁的时间很长。这种情况下,就白白的浪费了许多CPU资源。
参考:自旋锁
HashMap介绍
HashMap根据键值对来存储,HashMap中可以存放null值和null键,HashMap是非线程安全的
在HashMap中的底层数组中,每个元素在jdk1.7及之前叫做Entry,而在jdk1.8之后人家又改名叫做Node。
get()方法的工作原理
如果key为null,则直接从哈希表的第一个位置table[0]对应的链表上查找。如果Key不为null,则先求key的hash值,根据Key的hash值找到在table中的索引,然后在该索引对应的单链表中查找是否有键值对key与目标Key相等,如果有就返回对应的value,没有返回null。
当两个key的hashcode相同的时候,先找到key在table中的位置,然后去linkedList中使用key.equals找到对应的节点。
put()方法的工作原理
如果Key为null,则将其添加到table[0]对应的链表中,如果Key不为 null,则同样先求出Key的hash值,根据求得的hash值,寻找在table中的链表索引,然后遍历对应的单链表,如果单链表中存在与目标key相等的键值对,则将新的value覆盖旧的value,且将旧的value返回,如果找不到与目标key相等的键值对,或者该单链表为空,则将该键值对插入到单链表的头结点位置。
为什么String,Interger这样的wrapper类适合作为键
String, Interger这样的wrapper类作为HashMap的键是再适合不过了,而且String最为常用。因为String是不可变的,也是final的,而且已经重写了equals()和hashCode()方法了。其他的wrapper类也有这个特点。不可变性是必要的,因为为了要计算hashCode(),就要防止键值改变,如果键值在放入时和获取时返回不同的hashcode的话,那么就不能从HashMap中找到你想要的对象。不可变性还有其他的优点如线程安全。两个不相等的对象有不同的hashcode的话,那么碰撞的几率就会小,HashMap的性能就会提高。
重新调整HashMap大小存在什么问题
两个线程同时调整HashMap的大小的时候,会产生竞争
一个类的实例从new开始的过程
实例化一个对象时 ,首先会检查是否类已经加载并初始化,类加载并初始化之后,才会去创建实例。
寻找类定义
jvm首先会在名为“方法区”的内存中寻找是否有对应的Class的定义,并根据定义,例如生成一个名为MyObject的class对象。
加载类
如果“方法区”中没有名为“MyObject”的Class对象,jvm会用当前类的类加载器(classloader)从当前的classpath路径寻找名为"MyObject.class"的文件,如果找到,则将文件进行分析,转换为Class对象存放在“方法区”中,否则抛出“ClassNotFoundException”。对于jdk的class,jvm启动时,会用启动类加载器加载,对于用户的class,则会用应用程序类加载器实时加载,所谓实时加载,指的是遇到的时候再加载,而不是预先一次性加载。
给对象分配空间
找到MyObject的定义之后,在堆内存中为该对象分配空间,按照MyObject类的定义将各个属性设置为初始值。
java中类初始化顺序
class Parent {
// 静态变量
public static String p_StaticField = "父类--静态变量";
// 变量
public String p_Field = "父类--变量";
protected int i = 9;
protected int j = 0;
// 静态初始化块
static {
System.out.println(p_StaticField);
System.out.println("父类--静态初始化块");
}
// 初始化块
{
System.out.println(p_Field);
System.out.println("父类--初始化块");
}
// 构造器
public Parent() {
System.out.println("父类--构造器");
System.out.println("i=" + i + ", j=" + j);
j = 20;
}
}
class SubClass extends Parent {
// 静态变量
public static String s_StaticField = "子类--静态变量";
// 变量
public String s_Field = "子类--变量";
// 静态初始化块
static {
System.out.println(s_StaticField);
System.out.println("子类--静态初始化块");
}
// 初始化块
{
System.out.println(s_Field);
System.out.println("子类--初始化块");
}
// 构造器
public SubClass() {
System.out.println("子类--构造器");
System.out.println("i=" + i + ",j=" + j);
}
// 程序入口
public static void main(String[] args) {
System.out.println("子类main方法");
new SubClass();
}
}
上面的代码输出结果如下:
父类–静态变量
父类–静态初始化块
子类–静态变量
子类–静态初始化块
子类main方法
父类–变量
父类–初始化块
父类–构造器
i=9, j=0
子类–变量
子类–初始化块
子类–构造器
i=9,j=20
查看代码运行的结果,可以得到如下两条结论:
-
静态变量和静态初始化块的初始化时间(属于类的,类初始化时会加载)::早于::普通变量和普通初始化块::早于::构造方法。
-
并不是完全父类初始化完毕之后,子类才开始初始化。实际上子类的静态变量和静态初始化块的初始化是在父类的变量、初始化块和构造器初始化之前就完成了。
java实现线程安全的方式
每个线程有自己的工作内存,线程的工作内存保存被线程使用到变量的主内存副本拷贝。线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存的变量。不同线程之间也不能直接访问对方工作内存中的变量,线程间变量值的传递通过主内存来完成。
使用使用synchronized实现线程安全
它本具有原子性和可见性的,所以如果使用了synchronize修饰的操作,那么就自带了可见性。
Synchronized保证可见性的原因:
在Java内存模型中,synchronized规定,线程在枷锁时,先清空工作内存→在主内存中拷贝最新变量的副本到工作内存→执行完代码→将更改后的共享变量的值刷新到主内存中→释放互斥锁。
使用原子类
在涉及到多线程的时候,我们可以使用java提供的原子类来进行操作,例如涉及到整数自增a++时,可以使用AtomicInteger类incrementAndGet()/getAndIncrement()方法实现。
使用Volatile
使用volatile我们可以实现可见性,保证该线程对这个变量的修改可以及时提现到另一个线程中。
使用ThreadLocal对各个线程进行隔离
使用ThreadLocal可以保证各个线程对ThreadLocal变量的修改互不影响,保证了线程安全。
使用其他的锁实现线程安全
还可以使用ReentrantLock实现线程安全。
uses-sdk中minSdkVersion,targetSdkVersion,maxSdKVersion
- minSdkVersion - 可运行应用的最低 Android 平台版本,如果 minSdkVersion 值大于系统版本,系统会阻止安装应用。
- targetSdkVersion - 指定运行应用的目标 API 级别。
- maxSdkVersion - 指定作为应用设计运行目标的最高 API 级别的整数,明该属性可能会导致您的应用在系统更新至更高 API 级别后从用户设备中移除,因为应用所支持的最高版本小于当前系统版本,系统会将你的应用移除
- compileSdkVersion - 编译版本:compileSdkVersion告诉gradle使用哪个版本AndroidSDK编译你的应用
当manifest中使用<user-sdk>标签声明了上述的几个版本之后,同时gradle中也有相应的配置的话,会以gradle中的为准。
参考:https://developer.android.com/studio/publish/versioning?hl=zh-cn
https://developer.android.com/guide/topics/manifest/uses-sdk-element#ApiLevels