转眼间做Android开发已经6年多了,慢慢地从一个小白成长为一个可以独当一面的工程师。面试过不少人,也尝试看过一些外面的机会,拿到过一些offer,也被一些公司拒之门外,所幸都以平常心看待,遭遇挫折后决心沉淀下来继续奋战。偶有小小成就也不敢沾沾自喜,做工程师我认为最重要的还是要努力去沉淀,培养自己技术上的自信!只要技术积累足够,何愁没有好的归宿呢?当然技术积累是一个漫长的过程,要坐得住冷板凳,持之以恒,只要坚持下去,迟早会有所成。
闲言少叙。下面是总结的一些高频知识点以及答案,结合自己的理解,参考了一些同行的博客,都服了链接供大家参考,如有疑问,欢迎留言探讨。
1 简单描述一下handler的通信原理?
Android为了线程安全,不允许在UI线程外的子线程操作UI。多线程的应用场景中,将工作线程中需更新UI
的操作信息 传递到 UI
主线程,从而实现 工作线程对UI
的更新处理,最终实现异步消息的处理。
Handler 的背后有着 Looper 以及 MessageQueue 的协助,三者通力合作,分工明确。
它们的职责如下:
Looper :负责关联线程以及消息的分发在该线程下从 MessageQueue 获取 Message,分发给 Handler ;
MessageQueue :是个队列,是个队列,负责消息的入队出队、存储与管理,负责管理由 Handler 发送过来的 Message 。
Handler : 负责发送并处理消息,面向开发者,提供 API,并隐藏背后实现的细节。
Looper: 负责回调消息到 handleMessage()。
Handler是如何实现线程之间的切换的呢?
例如现在有A、B两个线程,在A线程中有创建了handler,然后在B线程中调用handler发送一个message。
通过上面的分析我们可以知道,当在A线程中创建handler的时候,同时创建了MessageQueue与Looper,Looper在A线程中调用loop进入一个无限的for循环从MessageQueue中取消息,当B线程调用handler发送一个message的时候,会通过msg.target.dispatchMessage(msg);将message插入到handler对应的MessageQueue中,Looper发现有message插入到MessageQueue中,便取出message执行相应的逻辑,因为Looper.loop()是在A线程中启动的,所以则回到了A线程,达到了从B线程切换到A线程的目的。
2 handler导致的内存泄漏是什么原因?如何解决?
原因分析:
Handler 允许我们发送延时消息,如果在延时期间用户关闭了 Activity,那么该 Activity 会泄露。这个泄露是因为 Message 会持有 Handler,而又因为 Java 的特性,内部类会持有外部类的引用,使得 Activity 会被 Handler 持有,这样最终就导致 Activity 泄露。
解决办法:
1将 Handler 定义成静态的内部类,在内部持有 Activity 的弱引用,
2 在 Activity.onDestroy()
前移除消息。
示例代码如下:
private static class UserHandler extends Handler {
private WeakReference<HandlerActivity> ref;
public SafeHandler(HandlerActivity activity) {
this.ref = new WeakReference(activity);
}
@Override
public void handleMessage(final Message msg) {
HandlerActivity activity = ref.get();
if (activity != null)
{
activity.handleMessage(msg);
}
}
}
@Override
protected void onDestroy() {
userHandler.removeCallbacksAndMessages(null);
super.onDestroy();
}
3 Handler中的looper死循环为什么没有阻塞主线程?
线程默认没有Looper的,如果需要使用Handler就必须为线程创建Looper。我们经常提到的主线程,也叫UI线程,它就是ActivityThread,ActivityThread被创建时在起main方法中就会初始化了Looper,这也是在主线程中默认可以使用Handler的原因。
在子线程中,如果手动为其创建了Looper,那么在所有的事情完成以后应该调用quit方法来终止消息循环,否则这个子线程就会一直处于等待(阻塞)状态,而如果退出Looper以后,这个线程就会立刻(执行所有方法并)终止,因此建议不需要的时候终止Looper。
一般work线程任务执行完我们就希望它退出,但主线程我们不希望退出,它实际上是一个死循环。当然并非简单地死循环,如果消息需要处理时会自动休眠。主线程通过创建新线程的方式去处理任务,会导致主线程卡死或掉帧的原因是在Activity的OnCrate()/OnResume()/OnStart()回调中做耗时操作。
当主线程的MessageQueue没有消息时,便阻塞在loop的queue.next()方法里,此时主线程会释放CPU资源进入休眠状态,直到下个消息到达或者有事务发生,主线程被唤醒。 所以说,主线程大多数时候都是处于休眠状态,并不会消耗大量CPU资源。
4 Binder通信原理与机制?实现一次拷贝的流程
参考博客:https://blog.csdn.net/AndroidStudyDay/article/details/93749470
首先介绍一下内核空间(Kernel),内核空间是系统内核运行的空间,用户空间(User Space)是用户程序运行的空间。为了保证安全性,它们之间是隔离的。
linux操作系统中,因进程隔离,进程与进程间内存是不共享的。进程之间要进行数据交互就得采用IPC的方式(共享内存、管道
、socket、文件)。
系统调用是用户空间访问内核空间的唯一方式,保证了所有的资源访问都是在内核的控制下进行的,避免了用户程序对系统资源的越权访问,提升了系统安全性和稳定性。
传统的 IPC 方式:
消息发送方将要发送的数据存放在内存缓存区中,通过系统调用进入内核态。然后内核程序在内核空间分配内存,开辟一块内核缓存区,调用 copyfromuser() 函数将数据从用户空间的内存缓存区拷贝到内核空间的内核缓存区中。同样的,接收方进程在接收数据时在自己的用户空间开辟一块内存缓存区,然后内核程序调用 copytouser() 函数将数据从内核缓存区拷贝到接收进程的内存缓存区。这样数据发送方进程和数据接收方进程就完成了一次数据传输,我们称完成了一次进程间通信。
缺点:
1 效率低下, 2 次数据拷贝
2接收数据的缓存区由数据接收进程提供,但是接收进程并不知道需要多大的空间来存放将要传递过来的数据,因此只能开辟尽可能大的内存空间或者先调用 API 接收消息头来获取消息体的大小,这两种做法不是浪费空间就是浪费时间。
Binder通信原理
跨进程通信是需要内核空间做支持的,通常都是通过内核支持来实现进程间通信,但是Binder 并不是 Linux 系统内核的一部分。借助于 Linux 的动态内核可加载模块(Loadable Kernel Module,LKM)的机制;模块是具有独立功能的程序,它可以被单独编译,但是不能独立运行。它在运行时被链接到内核作为内核的一部分运行。这样,Android 系统就可以通过动态添加一个内核模块运行在内核空间,用户进程之间通过这个内核模块作为桥梁来实现通信。
Android 系统中,这个运行在内核空间,负责各个用户进程通过 Binder 实现通信的内核模块就叫 Binder 驱动(Binder Dirver)。
Binder IPC 机制中涉及到的内存映射通过 mmap() 来实现,mmap() 是操作系统中一种内存映射的方法。内存映射简单的讲就是将用户空间的一块内存区域映射到内核空间。映射关系建立后,用户对这块内存区域的修改可以直接反应到内核空间;反之内核空间对这段区域的修改也能直接反应到用户空间。
内存映射能减少数据拷贝次数,实现用户空间和内核空间的高效互动。两个空间各自的修改能直接反映在映射的内存区域,从而被对方空间及时感知。也正因为如此,内存映射能够提供对进程间通信的支持。
Binder 是基于 C/S 架构的。由一系列的组件组成,包括 Client、Server、ServiceManager、Binder 驱动。其中 Client、Server、Service Manager 运行在用户空间,Binder 驱动运行在内核空间。其中 Service Manager 和 Binder 驱动由系统提供,而 Client、Server 由应用程序来实现。Client、Server 和 ServiceManager 均是通过系统调用 open、mmap 和 ioctl 来访问设备文件 /dev/binder,从而实现与 Binder 驱动的交互来间接的实现跨进程通信。
Client、Server、ServiceManager、Binder 驱动这几个组件在通信过程中扮演的角色就如同互联网中服务器(Server)、客户端(Client)、DNS域名服务器(ServiceManager)以及路由器(Binder 驱动)之前的关系。
注:域名服务器:域名和IP地址的转换
路由器:连接两个网络设备
5 ART虚拟机和Dalvik虚拟机的区别?
介绍ART虚拟机和Dalvik虚拟机前,先看看JVM虚拟机,JVM本质上就是一个软件,是计算机硬件的一层软件抽象,在这之上才能够运行Java程序,JAVA在编译后会生成类似于汇编语言的JVM字节码,与C语言编译后产生的汇编语言不同的是,C编译成的汇编语言会直接在硬件上跑,但JAVA编译后生成的字节码是在JVM上跑,需要由JVM把字节码翻译成机器指令,才能使JAVA程序跑起来。
Dalvik Virtual Machine,是安卓中使用的虚拟机,所有安卓程序都运行在安卓系统进程里,每个进程对应着一个Dalvik虚拟机实例。他们都提供了对象生命周期管理、堆栈管理、线程管理、安全和异常管理以及垃圾回收等重要功能,各自拥有一套完整的指令系统。
Android4.4以上开始使用ART虚拟机,在此之前我们一直使用的Dalvik虚拟机。
Android4.4及以前使用的都是Dalvik虚拟机,我们知道Apk在打包的过程中会先将java等源码通过javac编译成.class文件,但是我们的Dalvik虚拟机只会执行.dex文件,这个时候dx会将.class文件转换成Dalvik虚拟机执行的.dex文件。Dalvik虚拟机在启动的时候会先将.dex文件转换成快速运行的机器码,又因为65535这个问题,导致我们在应用冷启动的时候有一个合包的过程,最后导致的一个结果就是我们的app启动慢,这就是Dalvik虚拟机的JIT特性(Just In Time)。
ART虚拟机是在Android5.0才开始使用的Android虚拟机,ART虚拟机必须要兼容Dalvik虚拟机的特性,但是ART有一个很好的特性AOT(ahead of time),这个特性就是我们在安装APK的时候就将dex直接处理成可直接供ART虚拟机使用的机器码,ART虚拟机将.dex文件转换成可直接运行的.oat文件,ART虚拟机天生支持多dex,所以也不会有一个合包的过程,所以ART虚拟机会很大的提升APP冷启动速度。
ART优点:
①系统性能显著提升
②应用启动更快、运行更快、体验更流畅、触感反馈更及时
③续航能力提升
④支持更低的硬件
ART缺点
①更大的存储空间占用,可能增加10%-20%
②更长的应用安装时间
总的来说ART就是“空间换时间”
6 如何做到进程保活?
参考博客:https://blog.csdn.net/cmyperson/article/details/89524921
系统如何杀死进程?
adj(重要性和优先级)值越大占用物理内存越多会被最先kill掉,OK,那么现在对于进程如何保活这个问题就转化成,如何降低oom_adj的值,以及如何使得我们应用占的内存最少。
进程分类
1 前台进程(Foreground process): 正在与用户交互的Activity(OnResume)
2 可见进程(Visible process) ; Activity(已调用 onPause())
3 服务进程(Service process) :和页面无关联
4 后台进程(Background process)
5 空进程(Empty process):优先干掉
保活方案
1、开启一个像素的Activity
基本思想,系统一般是不会杀死前台进程的。所以要使得进程常驻,我们只需要在锁屏的时候在本进程开启一个Activity,为了欺骗用户,让这个Activity的大小是1像素,并且透明无切换动画,在开屏幕的时候,把这个Activity关闭掉,所以这个就需要监听系统锁屏广播。
2 、前台服务
这方案实际利用了Android前台service的漏洞。
原理如下:
对于 API level < 18 :调用startForeground(ID, new Notification()),发送空的Notification ,图标则不会显示。
对于 API level >= 18:在需要提优先级的service A启动一个InnerService,两个服务同时startForeground,且绑定同样的 ID。Stop 掉InnerService ,这样通知栏图标即被移除。
3、相互唤醒
相互唤醒的意思就是,假如你手机里装了支付宝、淘宝、天猫、UC等阿里系的app,那么你打开任意一个阿里系的app后,有可能就顺便把其他阿里系的app给唤醒了。这个完全有可能的。此外,开机,网络切换、拍照、拍视频时候,利用系统产生的广播也能唤醒app,不过Android N已经将这三种广播取消了。
4、JobSheduler
JobSheduler是作为进程死后复活的一种手段,native进程方式最大缺点是费电, Native 进程费电的原因是感知主进程是否存活有两种实现方式,在 Native 进程中通过死循环或定时器,轮训判断主进程是否存活,当主进程不存活时进行拉活。其次5.0以上系统不支持。 但是JobSheduler可以替代在Android5.0以上native进程方式,这种方式即使用户强制关闭,也能被拉起来。
5、粘性服务&与系统服务捆绑
这个是系统自带的,onStartCommand方法必须具有一个int类型的返回值,这个int类型的返回值用来告诉系统在服务启动完毕后,如果被Kill,系统将如何操作,这种方案虽然可以,但是在某些情况or某些定制ROM上可能失效,我认为可以多做一种保保守方案。
7 序列化和反序列化的原理,Android的parcelable与serializable的区别?
两者都是支持序列化和反序列化的操作。
两者最大的区别在于 存储媒介的不同,Serializable 使用 I/O 读写存储在硬盘上,而 Parcelable 是直接 在内存中读写。很明显,内存的读写速度通常大于 IO 读写,所以在 Android 中传递数据优先选择 Parcelable。
Serializable 会使用反射,序列化和反序列化过程需要大量 I/O 操作, Parcelable 自已实现封送和解封(marshalled &unmarshalled)操作不需要用反射,数据也存放在 Native 内存中,效率要快很多。
8 View的绘制原理?
参考博客:https://blog.csdn.net/final__static/article/details/89240787
Window,ViewRootImpl,DecorView之间的联系:
Activity 包含一个Window,Window是一个抽象基类,是 Activity 和整个 View 系统交互的接口,只有一个实现子类PhoneWindow,提供了一系列窗口的方法,比如设置背景,标题等。
PhoneWindow 对应一个DecorView 跟 一个 ViewRootImpl,DecorView 是ViewTree 里面的顶层布局,是继承于FrameLayout,包含两个子View,一个id=statusBarBackground 的 View 和 LineaLayout,LineaLayout 里面包含 title 跟content,title就是平时用的TitleBar或者ActionBar, contenty也是FrameLayout,activity通过 setContent()加载布局的时候加载到这个View上。
ViewRootImpl就是建立 DecorView 和 Window 之间的联系。
View 绘制中measure,layout, draw 三个阶段:
measure :根据父 view 传递的 MeasureSpec 进行计算大小。
layout :根据 measure 子 View 所得到的布局大小和布局参数,将子View放在合适的位置上。
draw :把 View 对象绘制到屏幕上。
三个阶段的核心入口是在 ViewRootImpl 类的 performTraversals() 方法中。
performTraversals会依次调用performMeasure、performLayout和performDraw三个方法,这三个方法分别完成顶级View的measure、layout和draw这三大流程。其中performMeasure中会调用measure方法,在measure方法中又会调用onMeasure方法,在onMeasure方法中则会对所有子元素进行measure过程,这样就完成了一次measure过程;子元素会重复父容器的measure过程,如此反复完成了整个View数的遍历。
measure过程:决定了View的宽/高,完成后可通过getMeasuredWidthh和getMeasureHeight方法来获取View测量后的宽、高。
Layout过程决定了View的四个顶点的坐标和实际View的宽高,完成后可通过getTop、getBotton、getLeft和getRight拿到View的四个定点坐标。
Draw过程决定了View的显示,完成后View的内容才能呈现到屏幕上。
draw 过程中一共分成7步,其中两步我们直接直接跳过不分析了。
1:drawBackground(canvas): 作用就是绘制 View 的背景。
2:onDraw(canvas) :绘制 View 的内容。View 的内容是根据自己需求自己绘制的,所以方法是一个空方法,View的继承类自己复写实现绘制内容。
3:dispatchDraw(canvas):遍历子View进行绘制内容。在 View 里面是一个空实现,ViewGroup 里面才会有实现。在自定义 ViewGroup 一般不用复写这个方法,因为它在里面的实现帮我们实现了子 View 的绘制过程,基本满足需求。
4:onDrawForeground(canvas):对前景色跟滚动条进行绘制。
5:drawDefaultFocusHighlight(canvas):绘制默认焦点高亮.
9 事件的分发机制和滑动冲突?
参考博客; https://www.jianshu.com/p/38015afcdb58
当用户触摸屏幕时(View 或 ViewGroup派生的控件),将产生点击事件(Touch事件),Touch事件的相关细节(发生触摸的位置、时间等)被封装成MotionEvent对象,一般情况下,事件列都是以DOWN事件开始、UP事件结束,中间有无数的MOVE事件。事件分发其实是将事件(比如点击事件)(MotionEvent)传递到某个具体的View & 处理的整个过程。
一般情况下,事件列都是从用户按下(ACTION_DOWN)的那一刻产生的,不得不提到,三个非常重要的与事件相关的方法。
- dispatchTouchEvent()
- onTouchEvent()
- onInterceptTouchEvent()
事件传递的顺序:Activity -> ViewGroup(一组View的组合) -> View(TextView、Button)
需要总结的小点:
1、Android 事件分发总是遵循 Activity => ViewGroup => View 的传递顺序;
2、onTouch() 执行总优先于 onClick()。
滑动冲突
参考博客:https://www.jianshu.com/p/d82f426ba8f7
滑动冲突的基本形式分为两种,其他复杂的滑动冲突都可以拆成这两种基本形式:
- 1:外部滑动方向与内部方向不一致。
- 2:外部方向与内部方向一致。
第一种, 滑动方向不一致的情况。举个例子, 比如你用ViewPaper和Fragment搭配,而Fragment里往往是一个竖直滑动的ListView这种情况是就会产生滑动冲突,但是由于ViewPaper本身已经处理好了滑动冲突,所以我们无需考虑,不过若是换成ScrollView,我们就得自己处理滑动冲突了。
第二种,这种情况下,因为内部和外部滑动方向一致,系统会分不清你要滑动哪个部分,所以会要么只有一层能滑动,要么两层一起滑动得很卡顿。
处理办法
第一种:第一种的冲突主要是一个横向的,一个竖向的,所以在开发中我们只要判断滑动方向是竖向还是横向的,再让对应的View滑动即可。判断的方法有很多,比如竖直距离与横向距离的大小比较,哪个距离大就判定为向哪个方向滑动的;滑动路径与水平形成的夹角等等。
第二种:对于这种情况,比较特殊,我们没有通用的规则,得根据业务逻辑来得出相应的处理规则。举个最常见的例子,ListView下拉刷新功能,需要ListView自身滑动实现滑动,但是当滑动到头部时需要ListView和Header一起滑动,也就是整个父容器的滑动,这就涉及到滑动冲突问题了,如果不处理好滑动冲突,就会出现各种意想不到情况。对于这种情况的解决,我们可以采用拦截法:
- 1.外部拦截法(由父容器决定事件的传递):让事件都经过父容器的拦截处理(onInterceptTouchEvent ),如果父容器需要则拦截,如果不需要则不拦截,称为外部拦截法。
- 2:内部拦截法(自己决定事件的传递):父容器不拦截任何事件,将所有事件传递给子元素,如果子元素需要则消耗掉,如果不需要则通过requestDisallowInterceptTouchEvent方法(请求父类不要拦截,返回值为true时不拦截,返回值为false时为拦截)交给父容器处理,称为内部拦截法。
10 AsyncTask原理
AsyncTask中有两个线程池(SerialExecutor和THREAD_POOL_EXECUTOR)和一个Handler(InternalHandler),其中线程池SerialExecutor用于任务的排队,而线程池THREAD_POOL_EXECUTOR用于真正地执行任务,InternalHandler用于将执行环境从线程池切换到主线程。
InternalHandler是一个静态的Handler对象,为了能够将执行环境切换到主线程,这就要求sHandler这个对象必须在主线程创建。由于静态成员会在加载类的时候进行初始化,因此这就变相要求AsyncTask的类必须在主线程中加载,否则同一个进程中的AsyncTask都将无法正常工作。
11 HandlerThread的原理和使用。
参考博客; https://www.jianshu.com/p/9c10beaa1c95
HandlerThread的本质:继承Thread类 & 封装Handler类,本质上是一个Thrad。
HandlerThread的使用步骤分为5步:
// 步骤1:创建HandlerThread实例对象
// 传入参数 = 线程名字,作用 = 标记该线程
HandlerThread mHandlerThread = new HandlerThread("handlerThread");
// 步骤2:启动线程
mHandlerThread.start();
// 步骤3:创建工作线程Handler & 复写handleMessage()
// 作用:关联HandlerThread的Looper对象、实现消息处理操作 & 与其他线程进行通信
// 注:消息处理操作(HandlerMessage())的执行线程 = mHandlerThread所创建的工作线程中执行
Handler workHandler = new Handler( handlerThread.getLooper() ) {
@Override
public boolean handleMessage(Message msg) {
...//消息处理
return true;
}
});
// 步骤4:使用工作线程Handler向工作线程的消息队列发送消息
// 在工作线程中,当消息循环时取出对应消息 & 在工作线程执行相关操作
// a. 定义要发送的消息
Message msg = Message.obtain();
msg.what = 2; //消息的标识
msg.obj = "B"; // 消息的存放
// b. 通过Handler发送消息到其绑定的消息队列
workHandler.sendMessage(msg);
// 步骤5:结束线程,即停止线程的消息循环
mHandlerThread.quit();
注意事项; handler导致的内存泄漏问题
12 IntentService 的用法
参考博客:https://blog.csdn.net/b1480521874/article/details/94987641
IntentService,可以看做是Service和HandlerThread的结合体,在完成了使命之后会自动停止,适合需要在工作线程处理UI无关任务的场景。
- IntentService 是继承自 Service 并处理异步请求的一个类,在 IntentService 内有一个工作线程来处理耗时操作。
- 当任务执行完后,IntentService 会自动停止,不需要我们去手动结束。
- 如果启动 IntentService 多次,那么每一个耗时操作会以工作队列的方式在 IntentService 的 onHandleIntent 回调方法中执行,依次去执行,使用串行的方式,执行完自动结束。
注意:在IntentService的onCreate方法中开启了一个异步线程HandlerThread来处理我们的请求,并利用Looper和Handler来管理我们的请求命令队列。
启动 IntentService 为什么不需要新建线程?
IntentService内部的HandlerThread 继承自 Thread,内部封装了 Looper,在这里新建线程并启动,所以启动 IntentService 不需要新建线程。
为什么不建议通过 bindService() 启动 IntentService?
@Override
public IBinder onBind(Intent intent) {
return null;
}
IntentService 源码中的 onBind() 默认返回 null;不适合 bindService() 启动服务,如果你执意要 bindService() 来启动 IntentService,可能因为你想通过 Binder 或 Messenger 使得 IntentService 和 Activity 可以通信,这样那么 onHandleIntent() 不会被回调,相当于在你使用 Service 而不是 IntentService。
为什么多次启动 IntentService 会顺序执行事件,停止服务后,后续的事件得不到执行?
IntentService 中使用的 Handler、Looper、MessageQueue 机制把消息发送到线程中去执行的,所以多次启动 IntentService 不会重新创建新的服务和新的线程,只是把消息加入消息队列中等待执行,而如果服务停止,会清除消息队列中的消息,后续的事件得不到执行。
13 Merge、ViewStub 的作用。
Merge: 减少视图层级,可以删除多余的层级。
ViewStub: 按需加载,减少内存使用量、加快渲染速度、不支持 merge 标签
14 ThreadLocal的原理
ThreadLocal是一个关于创建线程局部变量的类。使用场景如下所示:
- 实现单个线程单例以及单个线程上下文信息存储,比如交易id等。
- 实现线程安全,非线程安全的对象使用ThreadLocal之后就会变得线程安全,因为每个线程都会有一个对应的实例。 承载一些线程相关的数据,避免在方法中来回传递参数。
当需要使用多线程时,有个变量恰巧不需要共享,此时就不必使用synchronized这么麻烦的关键字来锁住,每个线程都相当于在堆内存中开辟一个空间,线程中带有对共享变量的缓冲区,通过缓冲区将堆内存中的共享变量进行读取和操作,ThreadLocal相当于线程内的内存,一个局部变量。每次可以对线程自身的数据读取和操作,并不需要通过缓冲区与 主内存中的变量进行交互。并不会像synchronized那样修改主内存的数据,再将主内存的数据复制到线程内的工作内存。ThreadLocal可以让线程独占资源,存储于线程内部,避免线程堵塞造成CPU吞吐下降。
在每个Thread中包含一个ThreadLocalMap,ThreadLocalMap的key是ThreadLocal的对象,value是独享数据。
15 Android内存优化,使用SparseArray和ArrayMap代替HashMap
参考博客:https://blog.csdn.net/u010687392/article/details/47809295
HashMap
HashMap内部是使用一个默认容量为16的数组来存储数据的,而数组中每一个元素却又是一个链表的头结点,所以,更准确的来说,HashMap内部存储结构是使用哈希表的拉链结构(数组+链表)。
如果有多个元素key的hash值相同的话,后一个元素并不会覆盖上一个元素,而是采取链表的方式,把之后加进来的元素加入链表末尾,从而解决了hash冲突的问题,由此我们知道HashMap中处理hash冲突的方法是链地址法,在此补充一个知识点,处理hash冲突的方法有以下几种:
- 开放地址法
- 再哈希法
- 链地址法
- 建立公共溢出区
HashMap中默认的存储大小就是一个容量为16的数组,所以当我们创建出一个HashMap对象时,即使里面没有任何元素,也要分别一块内存空间给它。
当我们不断的向HashMap里put数据时,只要一满足扩容条件,HashMap的空间将会以2倍的规律进行增大。假如我们有几十万、几百万条数据,那么HashMap要存储完这些数据将要不断的扩容,而且在此过程中也需要不断的做hash运算,这将对我们的内存空间造成很大消耗和浪费。而且HashMap获取数据是通过遍历Entry[]数组来得到对应的元素,在数据量很大时候会比较慢。
所以在Android中,HashMap是比较费内存的,我们在一些情况下可以使用SparseArray和ArrayMap来代替HashMap。
SparseArray
SparseArray比HashMap更省内存,在某些条件下性能更好,主要是因为它避免了对key的自动装箱(int转为Integer类型),它内部则是通过两个数组来进行数据存储的,一个存储key,另外一个存储value,为了优化性能,它内部对数据还采取了压缩的方式来表示稀疏数组的数据,从而节约内存空间。
SparseArray只能存储key为int类型的数据,同时,SparseArray在存储和读取数据时候,使用的是二分查找法。
虽说SparseArray性能比较好,但是由于其添加、查找、删除数据都需要先进行一次二分查找,所以在数据量大的情况下性能并不明显,将降低至少50%。
满足下面两个条件我们可以使用SparseArray代替HashMap:
- 数据量不大,最好在千级以内
- key必须为int类型,这中情况下的HashMap可以用SparseArray代替。
ArrayMap
ArrayMap是一个<key,value>映射的数据结构,它设计上更多的是考虑内存的优化,内部是使用两个数组进行数据存储,一个数组记录key的hash值,另外一个数组记录Value值,它和SparseArray一样,也会对key使用二分法进行从小到大排序,在添加、删除、查找数据的时候都是先使用二分查找法得到相应的index,然后通过index来进行添加、查找、删除等操作,所以,应用场景和SparseArray的一样,如果在数据量比较大的情况下,那么它的性能将退化至少50%。
ArrayMap应用场景
- 数据量不大,最好在千级以内
- 数据结构类型为Map类型
总结
SparseArray和ArrayMap都差不多,使用哪个呢? 假设数据量都在千级以内的情况下:
1、如果key的类型已经确定为int类型,那么使用SparseArray,因为它避免了自动装箱的过程,如果key为long类型,它还提供了一个LongSparseArray来确保key为long类型时的使用。
2、如果key类型为其它的类型,则使用ArrayMap。
16 Thread、AsyncTask、IntentService的使用场景与特点。
- Thread线程,独立运行与于 Activity 的,当Activity 被 finish 后,如果没有主动停止 Thread或者 run 方法没有执行完,其会一直执行下去。
- AsyncTask 封装了两个线程池和一个Handler(SerialExecutor用于排队,THREAD_POOL_EXECUTOR为真正的执行任务,Handler将工作线程切换到主线程),其必须在 UI线程中创建,execute 方法必须在 UI线程中执行,一个任务实例只允许执行一次,执行多次抛出异常,用于网络请求或者简单数据处理。
- IntentService:处理异步请求,实现多线程,在onHandleIntent中处理耗时操作,多个耗时任务会依次执行,执行完毕自动结束。
17 什么是ANR 如何避免它?
在Android上,如果你的应用程序有一段时间响应不够灵敏,系统会向用户显示一个对话框,这个对话框称作应 用程序无响应(ANR:Application NotResponding)对话框。 用户可以选择让程序继续运行,但是,他们在使用你的 应用程序时,并不希望每次都要处理这个对话框。因此 ,在程序里对响应性能的设计很重要这样,这样系统就不会显 示ANR给用户。
不同的组件发生ANR的时间不一样,Activity是5秒,BroadCastReceiver是10秒,Service是20秒(均为前台)。
如果开发机器上出现问题,我们可以通过查看/data/anr/traces.txt即可,最新的ANR信息在最开始部分。
- 主线程被IO操作(从4.0之后网络IO不允许在主线程中)阻塞。
- 主线程中存在耗时的计算
- 主线程中错误的操作,比如Thread.wait或者Thread.sleep等 Android系统会监控程序的响应状况,一旦出现下面两种情况,则弹出ANR对话框
- 应用在5秒内未响应用户的输入事件(如按键或者触摸)
- BroadcastReceiver未在10秒内完成相关的处理
- Service在特定的时间内无法处理完成 20秒
修正:
1、使用AsyncTask处理耗时IO操作。
2、使用Thread或者HandlerThread时,调用Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND)设置优先级,否则仍然会降低程序响应,因为默认Thread的优先级和主线程相同。
3、使用Handler处理工作线程结果,而不是使用Thread.wait()或者Thread.sleep()来阻塞主线程。
4、Activity的onCreate和onResume回调中尽量避免耗时的代码。 BroadcastReceiver中onReceive代码也要尽量减少耗时,建议使用IntentService处理。
解决方案:
将所有耗时操作,比如访问网络,Socket通信,查询大 量SQL 语句,复杂逻辑计算等都放在子线程中去,然 后通过handler.sendMessage、runonUIThread、AsyncTask、RxJava等方式更新UI。无论如何都要确保用户界面的流畅 度。如果耗时操作需要让用户等待,那么可以在界面上显示度条。