1、假设让你设计一个图片加载器,你会如何设计?
ImageLoad大致流程如下:
1、检查内存缓存,如有,则返回。
2、后台线程开始后续工作
3、检查是否在未解码内存缓存中。如有,解码,变换,返回,然后缓存到内存缓存中
4、检查是否在磁盘缓存中,如有,变换,返回,然后缓存到未解码缓存和内存缓存中。
5、从网络或者本地加载。加载完成后,解码,变换,返回。存到各个缓存中。
2、Activity、Window、View三者的关系?
从Activity的源码中:Activity.attch() -> PolicyManager -> Policy -> PhoneWindow -> mLayoutInflater.inflate()&mContentParent.addView() 这知识一个简单的跟踪过程描述,通过源码就可以明确的看出三者之间的关系。
Activity是整个模型的控制单元,Window属于承载模型,负责承载视图,View是视图显示模型。
View是Android中的视图呈现方式,但是View不能单独存在,它必须依附在Window这个抽象的概念上面,因此有视图的地方就有Window。包括Activity,Dialog,Toast都是提供视图的地方,甚至依托Window而实现的视图,比如PopupWindow,Menu。有视图的地方就有Window,因此Activity、Dialog、Toast等视图都对应一个Window。
View绑定到Window上的:Window和View之间的额纽带是ViewRoot。ViewRoot对应于ViewRootImpl类,它是链接WindowManager和Decorview的纽带,View的三大流程(measure、layout、draw)均是通过ViewRoot来完成的。在ActivityThread中,当Activity对象被创建完成后,会将DecorView添加到Window中,同时会创建ViewRootImpl对象,并将ViewRootImpl对象和DecorView建立关联。
他们三者关系可以比喻为:Activity像个工匠(控制单元),Window像窗口(承载模型),View像窗花(显示视图)。
1)、一个Activiy构造的时候会初始化一个Window,准确的说是一个PhoneWindow。
2)、这个PhoneWindow有一个ViewRoot,ViewRoot是一个View或者说ViewGroup,是最初始得根视图。
3)、ViewRoot通过addView方法 来添加一个个View。比如TextView,Button等。
4)、这些View的时间监听,是由WindowManagerService来接受消息,并且回调Activity函数。比如onClickListener、onKeyDown等。
3、Fragment的生命周期
Fragment/Activity LifeCycle这张图是Fragment生命周期和Activity生命周期对比图,可以看出两者有很多相似的地方。比如都有onCreate(),onStart(),onPause(),onDestroy()等等,因为Fragment是被托管在Activity中的,所以多了两个onAttach()和onDetach()。
onAttach() -> Fragment 和Activity建立关联的时候调用,被附加到Activity中去。
onCreate() -> 系统会在创建Fragment的时候调用此方法。可以初始化一段资源文件等等
onCreateView() -> 系统会在Fragment首次绘制其用户界面的时候调用此方法。要想为Fragment绘制UI,从该方法中返回的VIew必须是Fragment布局的根视图。如果Fragment未提供UI,可以返回null
onViewCreated() -> 在Fragment被绘制后,调用此方法,可以初始化空间资源。
onActivityCreated() -> 当onCreate(),onCreateView(),onViewCreated()方法执行完后调用,也就是Activity被渲染绘制出来后。
onPause() -> 系统将此方法作为用户离开Fragment的第一个信号(但并不总是意味着此Fragment会被销毁)进行调用,通常可以在此方法内确认在当前用户会话结束后仍然有效的任何更改(因为用户可能不会返回)。
onDestroyView() -> Fragment中的布局被移除时调用。
onDetach() -> Fragment和Activity解除关联的时候调用。
注: 除了onCreateView,其他的所有方法如果重写,必须调用父类对于该方法的实现。
4、自定义Vuew是一种什么样的流程?
当Activity接收到焦点的时候,它会被请求绘制布局,该请求会Android framework 处理,绘制是从根节点开始,对布局树进行measure和draw。整个View树的绘制流程在ViewRoot.java类的performTravesals()函数展开,该函数所做的工作可简单概括为是否需要重新计算视图大小(measure)、是否需要重新安置视图的位置(layout)、以及是否需要重绘(draw)。
自定义View5、Adnroid中事件机制
1、基础知识
(1)所有Touch事件都被封装到MotionEvent对象,包括Touch的位置、事件、历史记录以及第几个手指(多点出发)等
(2)事件类型分为: ACTION_DOWN、ACTION_UP、ACTION_MOVE、ACTION_CANCEL、ACTION_POINTER_DOWN、ACTION_POINTER_UP,每个事件都是以ACTION_DOWN开始ACTION_UP结束。
(3)对事件的处理包括三类:传递--->dispatchTouchEvent()函数、拦截--->onInterceptTouchEvent()函数、消费--->onTouchEvent()函数和OnTouchListener()。
2、传递流程
(1)事件从Activity.dispatchTouchEvent()开始传递,只要没有被停止或者拦截,从最上层的View(ViewGroup)开始一直往下(子View)传递。子VIew 可以通用onTouchEvent()对事件进行处理。
(2)事件有父View(ViewGroup)传递给子View、ViewGroup可以通过onInterceptTouchEvent()对事件进行拦截,停止其往下传递。
(3)如果事件从上往下传递过程中一直没有被停止,且最底层子View没有消费事件,事件会被反向往上传递,这时父View(ViewGroup)可以进行消费,如果还是没有被消费的话,最后会到Activity的onTouch()函数。
(4)如果View没有对ACTION_DOWN进行消费,之后的其他事件不会传递过来。
(5) OnTouchListener 优先于onTouchEvent()对事件进行消费。
以上的消费即相应的函数返回值为 true
view不消费事件view消费事件
6、Android的启动模式:区别和应用场景
1)、standard :标准模式,系统默认模式。每次启动一个Activiy都会创建一个新的实例,不管这个实例是否已经存在。这个模式下,谁启动了Activity,那么这个Activity就运行在启动它的那个Activity所在的栈中。
场景:邮件客户端,在新建一个邮件的时候,适合新建一个新的实例。
2)、singleTop:栈顶复用模式。这种模式下,如果新的Activity已经位于任务栈顶,那么这个Activiy不会被重新创建,同时它的onNewIntent方法会被回掉,通过此方法的参数我们可以取出当前的请求信息。
场景:消息推送,通知栏弹出Notification,点击Notification添砖到指定的Activity,避免生成重复的页面。登录的时候,登录页面跳转主页,重复点击登录按钮,避免生成两个主页。从Activity A启动了一个service进行耗时操作,或者某种监听,这个时候按home键了,service收集到信息,要返回Activity A。
3)、singleTask:栈内复用模式。这是一种单利模式,在这种模式下,只要Activiy在一个栈中存在,那么多次启动此Activity都不会创建实例,和singleTop是一样,系统也会调用onNewIntent。还有一点,就是singleTask有clearTop的效果,会导致栈内已有的Activity全部出栈。
场景:提供给第三方应用调用的页面,做浏览器、微博之类的应用,浏览器的主要页面等等。程序的主页面,进入多层嵌套之后,一键退回,之前的打开的Activity全部出栈。
4)、singleInstance:单一实例模式。这是一种加强版的singleTask模式,它除了具有singleTask的所有特性意外,还加强了一点,那就是具有此模式的Activity只能单独位于一个任务栈中,比如Activty A是singIeInstance模式,当A启动后,系统会为它创建一个新的任务栈,然后A独自在这个新的任务栈中,由于栈内复用的特性,后续均不会创建新的Activiy,除非这个独特的任务栈被系统销毁。整个手机操作系统里面只有一个实例存在,不同的应用去打开这个Activity共享公用的同一个Activity。他会运行在自己单独,独立的任务栈里面,并且任务栈里面只有他一个实例存在。
场景:呼叫来电界面,打电话、发短信功能。闹钟提醒,将闹钟提醒和闹钟设置分离
7、Android 内存泄漏怎么处理、如何排查
1)、集合类泄露
集合类如果仅仅只有添加元素的方法,而没有相应的删除机制的,导致内存被占用。如果这个集合类是全局性的变量(比如类中的静态属性,全局性的map等即有静态引用或final一直指向它),那么没有相应的删除机制,很可能导致集合所占内存只增不减。
2)、单例造成的内存泄露
由于单例的静态特性使得其生命周期跟应用的生命周期一样长,所以如果使用不恰当的话,很容易造成内存泄漏。
3)非静态内部类创建静态实例造成的内存泄漏 匿名内部类 Handler 造成的内存泄漏
Handler的使用造成的内存泄漏问题应该说是最为常见了,很多时候我们为了避免ANR而不在主线程进行耗时操作,在处理网络任务或者封装一些请求回掉等API都借助Handler来处理,但是Handler不是万能的,对于Handler的使用代码编写一不规范即有可能造成内存泄漏。另外,我们知道Handler、Message和MessageQueue都是相互关联在一起的,万一Handler发送的Message尚未被处理,则该Message及发送它的Handler对象将被线程MessageQueue一直持有。 由于Handler属于TLS(Thread Local Storage)变量,生命周期和Activity是不一致的。因此这种实现方式一般很难保证跟View或者Activity的生命周期保持一致,故很容易导致无法正确释放。
尽量避免使用static成员变量 避免override finalize()
a、finalize方法被执行的时间不确定,不能依赖与它来释放紧缺的资源。时间不确定的原因是:虚拟机调用GC时间不确定, Finalize daemon线程被调度到的时间不确定。
b、finalize 方法只会被执行一次,即使对象被复活,如果已经执行过了finalize方法,再次被GC时也不会被执行了,原因是:含有finalize方法的object是在new的时候由虚拟机生成了一个finalize reference 在来引用到该Object的,而在finalize方法执行的时候,该object所对应的finalize Reference会被释放掉,即使在这个时候把object复活(即用强引用引用该object),再第二次被GC的时候由于没有了finalize reference与之对应,所以finalize 方法不会被执行。
c、含有Finalize方法的object需要至少经过两轮GC才可能被释放掉。
对于使用BraodcastReceiver、ContentObserver、File、游标Cursor、Stream、Bitmap等资源的使用,应该再Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。---------------------一些不良代码造成的内存压力。有些代码并不造成内存泄漏,但是它们,或是对没使用的内存没进行有效及时的释放,或是没有有效的利用已有的对象而是频繁的申请新内存。
8、反射(Reflection)和注解(Annotation)的作用和原理
1、什么是反射(Reflection)
Java类是被Java虚拟机加载,如果java类不被java虚拟机加载,就不能正常的运行,正常情况下,我们运行所有程序再编译期时候就已经把那个类被加载了。Java的 反射机制是在编译时并不确定是哪个类被加载了,而是再程序运行的时候才被加载、探知、自审。使用的是再编译期并不知道的类,这就是JAVA反射的特点。
2、反射的作用
Java反射机制它知道类的基本结构,这种将Java类结构探知的能力,我们称为Java类的“自审”。如eclipse中,一按点,编译工具就会自动的把该对象能够使用的所有的方法和属性全部都列出来,供用户进行选择,这就是利用反射机制,做到代码的智能提示。 ------>> 举个例子:假如有两个程序员,一个程序员在写程序的时候需要使用第二个程序员所写的类,但是第二个程序员还没完成他所写的类。那么第一个程序员的代码是不能通过编译的。此时,利用Java反射的机制,就可以让第一个程序员在没有得到第二个程序员所写的类的时候,来完成自身代码的编译。
3、反射使用场景
a、工厂模式:Factory类中用反射的话,添加了一个新的类之后,就不需要再修改工厂类Factory了。
b、数据库JDBC中通过Class.forName(Driver).来获得数据库的连接驱动
c、分析类文件:毕竟能得到类中的方法等等。
d、访问一些不能访问的变量和属性:破解别人代码
4、什么是注解
注解是Java 的一个新特性。注解是插入你代码中的一种注释或者说是一种元数据(meta data)。这些注解信息可以再编译期使用预编译工具进行处理(pre-compiler tools)。也可以在运行期使用java反射机制进行处理。
Annotation对程式运行没有影响,它的目的是让编译器或分析工具说明程式的某些咨询,您可以在包、类方法、成员等加上Annotation,每一个Annotation对应于一个实际的Annotation型态,您可以从java.lang.Override,java.lang.Deprecated、java.lang.SuppressWarnings这三个j2SE 中标准的Annotation型态开始了解Annotation的作用。
5、限定Override 父类方法@Override
java.lang.Override是J2SE 5.0中标准的Annotation型态直以,它对编译期说明某个方法必须是重新定义父类别中的方法,编译器得知这项资讯后,在编译程序时如果发现被@Override标示的方法并非重新定义父类别中的方法,就会报错。
java.lang.Override是Marker Annotation,简单的说就是用于标示的Annotation,Annotation名称本身即表示了要给工具程式的资讯,例如Override这个名称告知编译器,被@Override标示的方法必须是重新定义父类别中的同名方法。
6、标示方法Deprecated @Deprecated
java.lang.Deprecated也是Mark annotation,简单的说就是用于表示Annotaion名称本身即包括了要给工具程式的资讯,例如Deprecated这个名称告知编译器,被@Deprecated标示的方法是一个不建议被使用的方法,如果有开发人员不小心使用了被@Deprecated标示的方法,编译器要提出提示提醒开发人员。
7、抑制编译器提示@SuppressWarnings
java.lang.SuppressWarnings是J2SE 5.0中标准Annotation型态直以,它对编译期说明某个方法中若有提示讯息,则加以抑制,不用在编译完成后出现提示,不过事实上这个功能在Sun JDK 5.0中没有实现出来。
8、注解使用场景
设计一个原始码分析工具,分析代码等;日志信息打
9、进程和线程关系,在Android中activity A和activity B这两者是不是听一个线程中。
进程一般是指一个执行的单元,在PC和一定设备上指的是一个程序或者一个应用,一个进程可以包含多个进程,因此进程和线程是包含和被包含的关系,最简单的情况下,一个进程中可以只有一个线程,即主线程,在Android中主线程也叫UI线程,只有在Ui线程中才能操作界面元素,但不能执行耗时操作任务。 当某个应用组件启动且应用没有运行其他任何组件时,Android系统会使用单个执行线程为应用启动新的Linux进程。默认情况下,用一个应用的所有组件在相同的进程和线程(称为“主”线程)中运行。如果某个应用组件启动且该应用已存在进程(因为存在该应用的其他组件),则该组件会在此进程内启动并使用相同的执行线程。但是 你可以安排应用中的其他组件在单独的进程中运行,并为任何进程创建额外的线程。
1、进程
Android系统将尽量长时间地保持应用进程,但为了新建进程或运行更重要的进程,最终需要移除旧进程来回收内存。为了确定保留或终止那些进程,系统会根据进程中正在运行的组件以及这些组件的状态,将每个进程放入“重要性层次结构”中。必要时,系统会首先消除重要性最低的进程,然后时重要性略逊的进程,以此类推,回收系统资源。
2、线程
应用启动时,系统会为应用创建一个名为“主线程”的执行线程。此线程非常重要,因为它负责将时间分派给相应的用户界面小部件,其中包括绘图事件。此外,它也是应用也Android UI工具包组件(来自android.widget和android.view软件包的组件)进行交互的线程。因此主线程有时也称为UI线程。系统不会为每个组件实例创建单独的线程。运行于用一个进程的所有组件均在UI线程中实例化,并且对每个组件的系统调用均由该线程进行分派。因此,响应系统回调方法(例如:报告用户操作的onKeyDown或生命周期回调方法)始终在进程的UI线程中运行。
例如:当用户触摸屏幕上的按钮时,应用的UI线程会将触摸事件分派给小部件,而小部件反过来又设置其按下状态,并将失效请求发布到事件队列中。UI线程从队列中取消该请求并通知小部件应该重绘自身。
activity A和activity B是在相同的线程中,都被加载到UI主线程中。
10、hashmap的原理。hashcode和equal的区别
1、什么时候使用HashMap?它的特点
HashMap是基于Map接口的实现,存储键值对时,它可以接收null键值,是非同步的,HasMap存储的这Entry(hash, key,value,next)对象。
2、HashMap工作原理
通过hash的方法,通过put和get存储和获取对象。存储对象时,我们将K/V传给put方法时,它调用hashCode计算hash从而得到bucket位置,进一步存储,HashMap会根据当前bucket的占用情况自动调整容量(超过Load Facotr则resize为原来的2倍)。获取对象时,我们将K传给get,它调用hashCode计算hash从而得到bucket位置,并进一步调用equals()方法确定键值对。如果发生碰撞的时候,Hashmap通过链表将产生碰撞冲突的元素组织起来,在Java8中,如果一个bucket中碰撞冲突的元素超过某个限制(默认是8),则使用红黑树来代替链表,从而提高速度。
3、get和put的原理,equals()和hashCode()的作用
通过对key的hashCode()进行hashing,并计算下表(n-1 & hash),从而获取buckets的位置。如果产生碰撞,则利用key.equals()方法去链表或者树中查找对应的节点。
4、hash的实现
在Java 1.8的实现中,是通过hashCode()的高16位异或低16实现的:(h=k.hashCode())^(h>>>16),主要是从速度、功效、质量来考虑的,这么做可以在bucket的n比较小的时候,也能保证考虑到高低bit都参与到hash的计算中,同时也不会有太大的开销。
5、如果HashMap的大小超过了负载因子(load factor)定义的容量, 怎么办?
如果超过了负载因子(默认0.75),则会重新resize一个原来长度两倍的HasMap,并且重新调用hash方法。
String、Interger这样的wrapper类作为HashMap的键是再合适不过了,而且String最为常用。应为String是不可变的,也是final的,而且已经重写了equals()和hashCode()方法了。其他的wrapper类也有这个特点。不可变性是必要的,因为为了要计算hashCode(),就要防止键值改变,如果键值再放入时和获取时返回不同的hashcode的话,那么就不能从HashMap中找到你想要的对象。不可变性还有其他的有点如线程安全。如果你可以仅仅通过将某个field声明成final就能保证hashCode是不变的,那么就可以这么做。因为获取对象的时候需要同时equals()和hashCode()方法,那么键对象正确的重写是这个方法是非常重要的。如果两个不相等的对象返回不同的hashcode的话,那么碰撞的几率就会小些,这样就能提高HashMap的性能。 当然针对自定义对象也是可以作为键的,只要该对象遵守equals()和hashCode()方法的定义规则,并且当对象插入到Map中之后将不会发生改变。如果这个对象是不可变的那么它就满足了作为键的条件。
CocurrentHashMap和Hashtable
ConcurrentHashMap越来越多的使用,Hashtable是synchronized的,但是ConcurrentHashMap同步性能更好,因为它仅仅根据同步级别对map的一部分进行上锁。ConcurrentHashmap是可以替代Hashtable,但是HashTable提供更强的线程安全性。
hashcode和equal区别
在java中任何一个对象都具备equals(Object obj)和hashcode()这两个方法,因为他们是在Object类中定义的。
equals(Object obj)方法用来判断两个对象是否“相同”,如果“相同“则返回true,否则返回false
hashCode()方法返回一个int数,在Object类中默认实现是”将该对象的内部地址转换成一个整数返回“。
a、如果两个对象equals,java运行环境会认为他们hashcode一定相等。
b、如果两个对象不equls,他们的hashcode可能相等。
c、如果两个对象hashcode相等,他们不一定eqlus。
d、如果两个对象hashcode不相等,他们一定不equls。
11、设计模式六大原则
1、单一原则。就是说一个类只有一个明确的职责,不易过多职责封装在一个类里面。
2、开闭原则。对于修改是关闭的,对于扩展是开放的,这样主要目的是提高可扩展性。
3、依赖倒置原则。高层不依赖于底层,两者都依赖于抽象,抽象不依赖于细节实现,具体实现依赖于抽象,也就是说要面向接口编程。
4、里氏替换原则。也就是说子类运行的功能,父类也能运行,强调集成的重要性。
5、迪米特原则。一个类要了解另一个类最少的内容,强调低耦合,耦合分解。
6、接口隔离原则。一个类不要实现不需要的接口,接口可拆分,不要冗余在一个总接口中,实现自己所需的接口即可。
12、动态代理
代理是一种常用的设计模式,其目的就是为其他对象提供一个代理以控制对某个对象的访问。代理类负责为委托类预处理消息,过滤消息并转发消息,以及进行消息被委托类执行后的后续处理。
代理模式为了保护行为的一致性,带里类和委托类通常会实现相同的接口,所以在访问这看来两者没有丝毫的区别。通过代理类这中间层,能有效的控制对委托类对象的直接访问,也可以很好的隐藏和保护委托类对象,同时也为实施不同控制策略预留了空间,从而在设计上获得了更大的灵活性。Java动态代理机制以巧妙的方式近乎完美的实践了代理模式的设计理念。
静态代理:代理类是在编译时就实现好的。也就是说Java编译完成后代理类时一个实际的class文件
动态代理:代理类实在运行时生成的。也就是说Java编译完之后并没有实际的class文件,而是在运行时动态生成类字节码,并加载到JVM中。
一个典型的动态代理创建对象过程可分为四个步骤
1、通过实现InvocationHandler接口创建自己的调用处理器IvocationHandler handler = new InvocationHandlerImpl(....);
2、通过为Proxy类指定ClassLoader对象和一组interface创建动态代理类Class clazz = Proxy.getProxtClass(classLoader,new Class[]{......});
3、通过反射机制获取动态代理类的构造函数,其参数类型和调用处理器接口类型Constructor constructor = clazz.getConstructor(new Class[]{InvocationHandler.class});
4、通过构造行数创建代理类实例,此时需将调用处理器对象作为参数被传入Interface proxy=(Interface)constructor.newInstance(new Object[]{handler});
为了简化对象创建过程,Proxy类中的newInstance方法封装了2~4,只需要两步即可完成代理对象的创建。 生成的ProxySubject继承Proxy类实现了Subject接口,实现的Subject的方法实际调用处理器的invoke方法,而invoke方法利用反射调用的是被代理对象的方法(Object result= method.invoke(proxied,args))
Java实现动态代理的缺点:因为Java的单集成特性(每个代理类都继承了Proxy类),只能针对接口创建代理类,不能针对类创建代理类。
13、JVM、Java内存机制
JVM(java virtual machine)java虚拟机是运行Java程序必不可少的机制。JVM实现了Java语言最终要的特性:平台无关性。
编译后的Java程序指令并不直接在硬件西永的CPU上执行,而是由JVM执行。JVM屏蔽了与具体平台相关的信息,使Java语言编译程序只需要生成JVM上运行的目标字节码(.class),就可以在多种平台上不加修改的运行。Java虚拟机在执行字节码时,把字节码解释成具体平台上的机器指令执行。因此实现Java平台无关性。它是Java程序能在多平台间进行无缝移植的可靠保证,同时也是Java程序的安全检验引擎(还进行安全检查)。
JVM运行图14、android消息循环机制
Handler、Looper、Message 这三者都是Android异步消息处理线程相关的概念。异步消息处理线程启动后会进入一个无限循环体内,每循环一次,从其内部的消息队列中取出一个消息,然后回调相应的消息处理函数,执行完成一个消息后则继续循环。若消息队列为空,则线程会阻塞等待。
Android消息机制主要就是Handler的运行机制,Handler运行需要底层的MessageQueue和Looper支撑。其中MessageQueue采用的是单链表的结构,Looper可以叫做消息循环。由于MessageQueue只是一个消息存储单元,不能处理消息,而Looper就是专门处理消息的,Looper会以无限循环的形式去查找是否有新消息,如果有的话就处理,否则一直等待着。
Handler创建的时候会采用当前线程的Looper来构造消息循环系统,需要注意的是,线程默认没有Looper的,如果需要使用Handler就必须为线程创建Looper,因为默认的UI线程,也就是ActivityThread,ActivityThread被创建的时候就会初始化Looper,这也就是在主线程中默认可以使用Handler原因。
网友评论