Android 基础学习笔记(二)
Q:Android中有哪几种类型的动画?
1.View动画(View Animation)/补间动画(Tween animation):对View进行平移、缩放、旋转和透明度变化的动画,不能真正的>改变view的位置。应用如布局动画、Activity切换动画
2.逐帧动画(Drawable Animation):是View动画的一种,它会按照顺序播放一组预先定义好的图片
3.属性动画(Property Animation):对该类对象进行动画操作,真正改变了对象的属性
Q:帧动画在使用时需要注意什么?
使用祯动画要注意不能使用尺寸过大的图片,否则容易造成OOM
Q:View动画和属性动画的区别?
image.png
Q:View动画为何不能真正改变View的位置?而属性动画为何可以?
View动画改变的只是View的显示,而没有改变View的响应区域;而属性动画会通过反射技术来获取和执行属性的get、set方法,从而改变了对象位置的属性值。
Q:属性动画插值器和估值器的作用?
插值器(Interpolator):根据时间流逝的百分比计算出当前属性值改变的百分比。确定了动画效果变化的模式,如匀速变化、加速变化等等。View动画和属性动画均可使用。常用的系统内置插值器:
线性插值器(LinearInterpolator):匀速动画
加速减速插值器(AccelerateDecelerateInterpolator):动画两头慢中间快
减速插值器(DecelerateInterpolator):动画越来越慢
类型估值器(TypeEvaluator):根据当前属性改变的百分比计算出改变后的属性值。针对于属性动画,View动画不需要类型估值器。常用的系统内置的估值器:
整形估值器(IntEvaluator)
浮点型估值器(FloatEvaluator)
Color属性估值器(ArgbEvaluator)
Q:Activity、View、Window三者之间的关系?
围绕Window是Activity和View的桥梁展开
参考回答:在Activity启动过程其中的attach()方法中初始化了PhoneWindow,而PhoneWindow是Window的唯一实现类,然后Activity通过setContentView将View设置到了PhoneWindow上,而View通过WindowManager的addView()、removeView()、updateViewLayout()对View进行管理。
Q:Window有哪几种类型?
Window有三种类型:
应用Window:对应一个Activity。
子Window:不能单独存在,需附属特定的父Window。如Dialog。
系统Window: 需申明权限才能创建。如Toast。
Q:Activity创建和Dialog创建过程的异同?
Dialog的Window创建过程:
创建WindowDialog。和Activity类似,同样是通过PolicyManager.makeNewWindow() 来实现。
初始化DecorView并将Dialog的视图添加到DecorView中去。和Activity类似,同样是通过Window.setContentView() 来实现。
将DecorView添加到Window中显示。和Activity一样,都是在自身要出现在前台时才会将添加Window。
Dialog.show() 方法:完成DecorView的显示。
WindowManager.remoteViewImmediate() 方法:当Dialog被dismiss时移除DecorView。
Q:谈谈消息机制Hander?作用?有哪些要素?流程是怎样的?
技术点:消息机制
参考回答:
作用:跨线程通信。当子线程中进行耗时操作后需要更新UI时,通过Handler将有关UI的操作切换到主线程中执行。
四要素:
Message(消息):需要被传递的消息,其中包含了消息ID,消息处理对象以及处理的数据等,由MessageQueue统一列队,最终由Handler处理。
MessageQueue(消息队列):用来存放Handler发送过来的消息,内部通过单链表的数据结构来维护消息列表,等待Looper的抽取。
Handler(处理者):负责Message的发送及处理。通过 Handler.sendMessage() 向消息池发送各种消息事件;通过 Handler.handleMessage() 处理相应的消息事件。
Looper(消息泵):通过Looper.loop()不断地从MessageQueue中抽取Message,按分发机制将消息分发给目标处理者。
具体流程如图
image.png
Handler.sendMessage()发送消息时,会通过MessageQueue.enqueueMessage()向MessageQueue中添加一条消息;
通过Looper.loop()开启循环后,不断轮询调用MessageQueue.next();
调用目标Handler.dispatchMessage()去传递消息,目标Handler收到消息后调用Handler.handlerMessage()处理消息。
Q:为什么系统不建议在子线程访问UI?
系统不建议在子线程访问UI的原因是,UI控件非线程安全,在多线程中并发访问可能会导致UI控件处于不可预期的状态。而不对UI控件的访问加上锁机制的原因有:
上锁会让UI控件变得复杂和低效
上锁后会阻塞某些进程的执行
Q:一个Thread可以有几个Looper?几个Handler?
Looper、Handler
参考回答:一个Thread只能有一个Looper,可以有多个Handler
引申:更多数量关系:Looper有一个MessageQueue,可以处理来自多个Handler的Message;MessageQueue有一组待处理的Message,这些Message可来自不同的Handler;Message中记录了负责发送和处理消息的Handler;Handler中有Looper和MessageQueue;
Q:如何将一个Thread线程变成Looper线程?Looper线程有哪些特点?
通过Looper.prepare()可将一个Thread线程转换成Looper线程。Looper线程和普通Thread不同,它通过MessageQueue来存放消息和事件、Looper.loop()进行消息轮询。
Q:可以在子线程直接new一个Handler吗?那该怎么做?
不同于主线程直接new一个Handler,由于子线程的Looper需要手动去创建,在创建Handler时需要多一些方法:
new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();//为子线程创建Looper
new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
//子线程消息处理
}
};
Looper.loop(); //开启消息轮询
}
}).start();
Q:Message可以如何创建?哪种效果更好,为什么?
创建Message对象的几种方式:
Message msg = new Message();
Message msg = Message.obtain();
Message msg = handler1.obtainMessage();
后两种方法都是从整个Messge池中返回一个新的Message实例,能有效避免重复Message创建对象,因此更鼓励这种方式创建Message
Q:这里的ThreadLocal有什么作用?
参考回答:ThreadLocal类可实现线程本地存储的功能,把共享数据的可见范围限制在同一个线程之内,无须同步就能保证线程之间不出现数据争用的问题,这里可理解为ThreadLocal帮助Handler找到本线程的Looper。
底层数据结构:每个线程的Thread对象中都有一个ThreadLocalMap对象,它存储了一组以ThreadLocal.threadLocalHashCode为key、以本地线程变量为value的键值对,而ThreadLocal对象就是当前线程的ThreadLocalMap的访问入口,也就包含了一个独一无二的threadLocalHashCode值,通过这个值就可以在线程键值值对中找回对应的本地线程变量。
Q:主线程中Looper的轮询死循环为何没有阻塞主线程?
Android是依靠事件驱动的,通过Loop.loop()不断进行消息循环,可以说Activity的生命周期都是运行在 Looper.loop()的控制之下,一旦退出消息循环,应用也就退出了。而所谓的导致ANR多是因为某个事件在主线程中处理时间太耗时,因此只能说是对某个消息的处理阻塞了Looper.loop(),反之则不然。
Q:使用Hanlder的postDealy()后消息队列会发生什么变化?
post delay的Message并不是先等待一定时间再放入到MessageQueue中,而是直接进入并阻塞当前线程,然后将其delay的时间和队头的进行比较,按照触发时间进行排序,如果触发时间更近则放入队头,保证队头的时间最小、队尾的时间最大。此时,如果队头的Message正是被delay的,则将当前线程堵塞一段时间,直到等待足够时间再唤醒执行该Message,否则唤醒后直接执行。
Q:Android中还了解哪些方便线程切换的类?
对Handler进一步的封装的几个类:
AsyncTask:底层封装了线程池和Handler,便于执行后台任务以及在子线程中进行UI操作。
HandlerThread:一种具有消息循环的线程,其内部可使用Handler。
IntentService:是一种异步、会自动停止的服务,内部采用HandlerThread。
引申:更多是对消息机制的理解
Q:AsyncTask相比Handler有什么优点?不足呢?
Handler机制存在的问题:多任务同时执行时不易精确控制线程。
引入AsyncTask的好处:创建异步任务更简单,直接继承它可方便实现后台异步任务的执行和进度的回调更新UI,而无需编写任务线程和Handler实例就能完成相同的任务。
Q:使用AsyncTask需要注意什么?
不要直接调用onPreExecute()、doInBackground()、onProgressUpdate()、onPostExecute()和onCancelled()方法
一个异步对象只能调用一次execute()方法
引申:谈谈AsyncTask初始化、五个核心方法如何配合进而体现Handler的作用
Q:AsyncTask中使用的线程池大小?
在AsyncTask内部实现有两个线程池:
SerialExecutor:用于任务的排队,默认是串行的线程池,在3.0以前核心线程数为5、线程池大小为128,而3.0以后变为同一时间只能处理一个任务
THREAD_POOL_EXECUTOR:用于真正执行任务。
引申:谈谈对线程池的理解
Q:HandlerThread有什么特点?
HandlerThread是一个线程类,它继承自Thread。与普通Thread不同,HandlerThread具有消息循环的效果,这是因为它内部HandlerThread.run()方法中有Looper,能通过Looper.prepare()来创建消息队列,并通过Looper.loop()来开启消息循环。
Q:快速实现子线程使用Handler
不同于之前手动在子线程创建Looper再构建Handler的想法,这里从HandlerThread角度去快速实现在子线程使用Handler
参考回答:HandlerThread实现方法
实例化一个HandlerThread对象,参数是该线程的名称;
通过 HandlerThread.start()开启线程;
实例化一个Handler并传入HandlerThread中的looper对象,使得与HandlerThread绑定;
利用Handler即可执行异步任务;
当不需要HandlerThread时,通过HandlerThread.quit()/quitSafely()方法来终止线程的执行。
Q:IntentService的特点?
和普通线程和普通Service比较突出其特点
参考回答: 不同于线程,IntentService是服务,优先级比线程高,更不容易被系统杀死,因此较适合执行一些高优先级的后台任务;不同于普通Service,IntentService可自动创建子线程来执行任务,且任务执行完毕后自动退出。
Q:为何不用bindService方式创建IntentService?
技术点:IntentService
思路:从底层实现出发
参考回答:IntentService的工作原理是,在IntentService的onCreate()里会创建一个HandlerThread,并利用其内部的Looper实例化一个ServiceHandler对象;而这个ServiceHandler用于处理消息的handleMessage()方法会去调用IntentService的onHandleIntent(),这也是为什么可在该方法中处理后台任务的逻辑;当有Intent任务请求时会把Intent封装到Message,然后ServiceHandler会把消息发送出,而发送消息是在onStartCommand()完成的,只能通过startService()才可走该生命周期方法,因此不能通过bindService创建IntentService。
Q:线程池的好处、原理、类型?
(1)线程池的好处:
* 重用线程池中的线程,避免线程的创建和销毁带来的性能消耗;
* 有效控制线程池的最大并发数,避免大量的线程之间因互相抢占系统资源而导致阻塞现象;
* 进行线程管理,提供定时/循环间隔执行等功能* (2)线程池的分类:
* FixThreadPool:线程数量固定的线程池,所有线程都是核心线程,当线程空闲时不会被回收;能快速响应外界请求。
* CachedThreadPool:线程数量不定的线程池(最大线程数为Integer.MAX_VALUE),只有非核心线程,空闲线程有超时机制,超时回收;适合于执行大量的耗时较少的任务
* ScheduledThreadPool:核心线程数量固定,非核心线程数量不定;可进行定时任务和固定周期的任务。
* SingleThreadExecutor:只有一个核心线程,可确保所有的任务都在同一个线程中按顺序执行;好处是无需处理线程同步问题。* (3)线程池的原理:实际上通过ThreadPoolExecutor并通过一系列参数来配置各种各样的线程池,具体的参数有:
* corePoolSize核心线程数:一般会在线程中一直存活
* maximumPoolSize最大线程数:当活动线程数达到这个数值后,后续的任务将会被阻塞
* keepAliveTime非核心线程超时时间:超过这个时长,闲置的非核心线程就会被回收
* unit:用于指定keepAliveTime参数的时间单位
* workQueue任务队列:通过线程池的execute()
方法提交的Runnable对象会存储在这个参数中。
* threadFactory:线程工厂,可创建新线程
* handler:在线程池无法执行新任务时进行调度* 引申:使用Executors各个方法创建线程池的弊端
Q:ThreadPoolExecutor的工作策略?
ThreadPoolExecutor的默认工作策略:
若程池中的线程数量未达到核心线程数,则会直接启动一个核心线程执行任务。
若线程池中的线程数量已达到或者超过核心线程数量,则任务会被插入到任务列表等待执行。
若任务无法插入到任务列表中,往往由于任务列表已满,此时如果
线程数量未达到线程池最大线程数,则会启动一个非核心线程执行任务;
线程数量已达到线程池规定的最大值,则拒绝执行此任务,ThreadPoolExecutor会调用RejectedExecutionHandler的rejectedExecution方法来通知调用者。
引申:ThreadPoolExecutor的拒绝策略
Q:什么是ANR?什么情况会出现ANR?如何避免?在不看代码的情况下如何快速定位出现ANR问题所在?`
ANR(Application Not Responding,应用无响应):当操作在一段时间内系统无法处理时,会在系统层面会弹出ANR对话框
产生ANR可能是因为5s内无响应用户输入事件、10s内未结束BroadcastReceiver、20s内未结束Service
想要避免ANR就不要在主线程做耗时操作,而是通过开子线程,方法比如继承Thread或实现Runnable接口、使用AsyncTask、IntentService、HandlerThread等
引申:快读定位ANR方法:使用命令导出ANR日志,并分析关键信息,详见如何分析ANR
Q:加载图片的时候需要注意什么?
技术点:Bitmap高效加载
参考回答:
直接加载大容量的高清Bitmap很容易出现显示不完整、内存溢出OOM的问题,所以最好按一定的采样率将图片缩小后再加载进来
为减少流量消耗,可对图片采用内存缓存策略,又为了避免图片占用过多内存导致内存溢出,最好以软引用方式持有图片
如果还需要网上下载图片,注意要开子线程去做下载的耗时操作
Q:LRU算法的原理?
为减少流量消耗,可采用缓存策略。常用的缓存算法是LRU(Least Recently Used):
核心思想:当缓存满时, 会优先淘汰那些近期最少使用的缓存对象。主要是两种方式:
LruCache(内存缓存):LruCache类是一个线程安全的泛型类:内部采用一个LinkedHashMap以强引用的方式存储外界的缓存对象,并提供get和put方法来完成缓存的获取和添加操作,当缓存满时会移除较早使用的缓存对象,再添加新的缓存对象。
DiskLruCache(磁盘缓存): 通过将缓存对象写入文件系统从而实现缓存效果
引申:感兴趣可了解具体实现算法
Q:项目中如何做性能优化的?
举例说明项目注意了哪些方面的性能优化,如布局优化、绘制优化、内存泄漏优化、 响应速度优化、列表优化、Bitmap优化、 线程优化......
Q:了解哪些性能优化的工具?
性能优化工具
思路:做项目时是否使用过的系统自带的性能优化工具?公司是否有自己的性能优化工具?实现原理怎样的?
Q:布局上如何优化?
布局优化的核心就是尽量减少布局文件的层级,常见的方式有:
多嵌套情况下可使用RelativeLayout减少嵌套。
布局层级相同的情况下使用LinearLayout,它比RelativeLayout更高效。
使用<include>标签重用布局、<merge>标签减少层级、<ViewStub>标签懒加载。
Q:内存泄漏是什么?为什么会发生?常见哪些内存泄漏的例子?都是怎么解决的?
内存泄漏(Memory Leak)是指程序在申请内存后,无法释放已申请的内存空间。简单地说,发生内存泄漏是由于长周期对象持有对短周期对象的引用,使得短周期对象不能被及时回收。常见的几个例子和解决办法:
单例模式导致的内存泄漏:单例传入参数this来自Activity,使得持有对Activity的引用。
解决办法:传参context.getApplicationContext()
Handler导致的内存泄漏:Message持有对Handler的引用,而非静态内部类的Handler又隐式持有对外部类Activity的引用,使得引用关系会保持至消息得到处理,从而阻止了Activity的回收。
解决办法:使用静态内部类+WeakReference弱引用;当外部类结束生命周期时清空消息队列。
线程导致的内存泄漏:AsyncTask/Runnable以匿名内部类的方式存在,会隐式持有对所在Activity的引用。
解决办法:将AsyncTask和Runnable设为静态内部类或独立出来;在线程内部采用弱引用保存Context引用
资源未关闭导致的内存泄漏:未及时注销资源导致内存泄漏,如BraodcastReceiver、File、Cursor、Stream、Bitmap等。
解决办法:在Activity销毁的时候要及时关闭或者注销。
BraodcastReceiver:调用unregisterReceiver()注销;
Cursor,Stream、File:调用close()关闭;
动画:在Activity.onDestroy()中调用Animator.cancel()停止动画
引申:谈谈项目中是如何注意内存泄漏的问题
内存泄漏和内存溢出的区别
内存泄漏(Memory Leak)是指程序在申请内存后,无法释放已申请的内存空间。是造成应用程序OOM的主要原因之一。
内存溢出(out of memory)是指程序在申请内存时,没有足够的内存空间供其使用。
Q:什么情况会导致内存溢出?
内存泄漏是导致内存溢出的主要原因;直接加载大图片也易造成内存溢出
引申:谈谈如何避免内存溢出(如何避免内存泄漏、避免直接加载大图片)
开源框架(略)
Q:使用那些版本控制工具?Git和SVN的区别?
参考回答:Git和SVN的区别有以下几点:
Git是分布式的,而SVN是集中式的(核心区别)
Git按元数据方式存储内容,而SVN按文件存储内容
在Git上每个工作成员可以任意在自己的本地版本库开启无限个分支且互不影响,而对于SVN分支是一个完整的目录且这个目录拥有完整的实际文件
Git没有一个全局的版本号,而SVN有
Git 的内容完整性要优于SVN
在Git中的绝大多数操作都只需要访问本地文件和资源,不必联网就可以看到所有的历史版本记录,而SVN 却需要联网
引申:谈谈两种版本控制工具的优缺点:SVN与GIT的优缺点对比
Q:了解Git工具吗?用过哪些命令?解决冲突时git merge和git rebase的区别?
通过图记忆Git常用命令,详见Git、GitHub、Stash
image.png
参考回答: 常用命令见图,源自一篇文章,教你学会Git
合并用到的命令git merge与git rebase的区别是,git merge会生成一个新的节点,并将之前的提交分开显示;git rebase操作不会生成新的节点,而是将两个分支
融合成一个线性的提交。