强记知识点

JAVA new对象以及类加载过程:

如果是第一次使用该类,new一个对象可以分为两个过程:加载并初始化类和创建对象;否则就直接创建对象

一、类的加载过程:

采用的是双亲委派模式:

如果一个类加载器(引导加载器,拓展加载器,系统类加载器,自定义加载器)收到了 Class 加载的请求,它首先不会自己去尝试加载这个 Class ,而是把请求委托给父加载器去完成,依次向上。因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它的搜索范围中没有找到所需的 Class 时,即无法完成该加载,子加载器才会尝试自己去加载该 Class 。
使用双亲委托机制的好处是: ①.  java类随着它的类加载器一起具备了一种带有优先级的层次关系,通过这种层级关系可以避免类的重复加载,当父亲已经加载了该类时,就没有必要子classloader再加载一次。②  其次是考虑到安全因素,java核心api中定义类型不会被随意替换,假设通过网络传递一个名为java.lang.Integer的类,发现该类已被加载,并不会重新加载网络传递的过来的java.lang.Integer,而直接返回已加载过的Integer.class,这样便可以防止核心API库被随意篡改。

1.加载

由类加载器负责根据一个类的全限定名来读取此类的二进制字节流到JVM内部,并存储再运行时内存区的方法区,然后将其转换为一个与目标类型对应的java.lang.class对象实例

2.验证

格式验证:验证是否符合class文件规范

语义验证:检查一个被标记为final的类型是否包含子类,检查一个类中的final方法是否被子类进行重写;

确保父类和子类之间没有不兼容的一些方法声明(比如方法签名相同,但是方法的返回值不同)

操作验证:在操作数栈中的数据必须进行正确的操作,对常量池中的各种符号引用执行验证(通常在解析阶段执行,检查是否可以通过符号引用中描述的全限定名定位到指定类型上,以及类成员信息的访问修饰符是否允许访问等)

3.准备

为类中的所有静态变量分配内存空间,并为其设置一个初始值(由于还没有产生对象,实例变量不在此操作范围内)被final修饰的static变量(常量),会直接赋值;

4.解析

将常量池中的符号引用转为直接引用(得到类或者字段、方法在内存中的指针或者偏移量,以便直接调用该方法),这个可以在初始化之后在执行。

解析需要静态绑定的内容,//所有不会被重写的方法和域都会被静态绑定

2、3、4三个阶段又合称为链接阶段,链接阶段要做的是将加载到JVM中的二进制字节流的类数据信息合并到JVM的运行时状态中。

5.初始化(先静后非静,先父后子,先块后器

作用是为静态变量开辟内存

初始化顺序:父类静态成员变量,父类静态代码块,子类静态成员变量,子类静态代码块,父类非静态成员变量,父类非静态代码块,父类构造函数,子类非静态成员变量,子类非静态代码块,子类构造函数

二、创建对象

1.在堆分配内存

分配的内存包括本类和父类的所有实例变量,但不包括任何静态变量。

2.对所有实例变量赋默认值

将方法区内对实例变量的定义拷贝一份到堆区,然后赋默认值

3.执行实例初始化代码

初始化顺序是先初始化父类在初始化子类,初始化时先执行实例代码然后是构造方法

4.如果有类似于Child c = new Child()形式的c引用的话,在栈区定义Child类型引用变量c,然后将堆区对象的地址赋值给它;

Android系统结构:


Android系统体系结构(采用软件叠层,降低系统5层之间的耦合性):

1. 应用程序层--------就是手机上的App

2.应用程序框架

3.函数库-------------里面都是C/C++库,不能直接调用这些库,但是通过应用程序框架可以运行

4.Android运行时---------分为两部分1.Android核心库集,即Java语言核心库所使用的大部分功能

                                                     2.Dalvik虚拟机,负责运行ANdroid应用,每个Android应用程序都运行在单独的Dalvik虚拟机内,即每个应用都对应着已调dalvik进程,Dalvik擅长于多个虚拟机同时运行;Dalvik不同于Java中的JVM,因为JVM直接从.class文件中或JAR包加载字节码直接运行,而Dalvik不能,他需要从.class中通过DX工具将其转化为.dex文件(JVM运行.class文件,Dalvik运行.dex文件);

JVM:编译之后直接生成.class文件  (.class-->jar)运行这两种

DVM:.编译之后直接生成.class文件  .class-->(.dex)运行这一种,通过dx工具将。class文件转为。dex文件

5.Linux内核:

DVM指dalvik的虚拟机.每一个Android应用程序都在它自己的进程中运行,都拥有一个独立的 Dalvik虚拟机实例.而每一个DVM都是在Linux 中的一个进程,所以说可以认为是同一个概念.

应用启动流程 Window创建流程

点击图标中,分析AMS的工作流程,其实就是Activity的启动和调度的过程,所有的启动方式,最终都是通过Binder机制的Client端,调用Server端的AMS的startActivityXXX()系列方法。所以可见,工作流程又包括Client端和Server端两个。

1:客户端工作流程

1. Launcher主线程捕获onClick()点击事件后,调用Launcher.startActivitySafely()方法。Launcher.startActivitySafely()内部调用了Launcher.startActivity()方法,Launcher.startActivity()内部调用了Launcher的父类Activity的startActivity()方法。

2. Activity.startActivity()调用Activity.startActivityForResult()方法,传入该方法的requestCode参数若为-1,则表示Activity启动成功后,不需要执行Launcher.onActivityResult()方法处理返回结果。

3. 启动Activity需要与系统ActivityManagerService交互,必须纳入Instrumentation的监控,因此需要将启动请求转交instrumentation,即调用Instrumentation.execStartActivity()方法。

4. Instrumentation.execStartActivity()首先通过ActivityMonitor检查启动请求,然后调用ActivityManagerNative.getDefault()得到ActivityManagerProxy代理对象,进而调用该代理对象的startActivity()方法。

5. ActivityManagerProxy是ActivityManagerService的代理对象,因此其内部存储的是BinderProxy,调用ActivityManagerProxy.startActivity(),实质是调用BinderProxy.transact()向Binder驱动发送START_ACTIVITY_TRANSACTION命令。Binder驱动将处理逻辑从Launcher所在进程切换到ActivityManagerService所在进程。

2:服务端工作流程

启动Activity的请求从Client端传递给Server端后,便进入了启动应用的七个阶段,这里也是整理出具体流程。

2.1 预启动
ActivityManagerService.startActivity()
ActivityStack.startActivityMayWait()
ActivityStack.startActivityLocked()
ActivityStack.startActivityUncheckedLocked()
ActivityStack.startActivityLocked()(重载)
ActivityStack.resumeTopActivityLocked()
2.2 暂停
ActivityStack.startPausingLocked()
ApplicationThreadProxy.schedulePauseActivity()
ActivityThread.handlePauseActivity()
ActivityThread.performPauseActivity()
ActivityManagerProxy.activityPaused()
completePausedLocked()
2.3 启动应用程序进程
第二次进入ActivityStack.resumeTopActivityLocked()
ActivityStack.startSpecificActivityLocked()
startProcessLocked()
startProcessLocked()(重载)
Process.start()
2.4 加载应用程序Activity
ActivityThread.main()
ActivityThread.attach()
ActivityManagerService.attachApplication()
ApplicationThread.bindApplication()
ActivityThread.handleBindApplication()
2.5 显示Activity
ActivityStack.realStartActivityLocked()
ApplicationThread.scheduleLaunchActivity()
ActivityThead.handleLaunchActivity()
ActivityThread.performLaunchActivity()
ActivityThread.handleResumeActivity()
ActivityThread.performResumeActivity()
Activity.performResume()
ActivityStack.completeResumeLocked()
2.6 Activity Idle状态的处理
2.7 停止源Activity
ActivityStack.stopActivityLocked()
ApplicationThreadProxy.scheduleStopActivity()
ActivityThread.handleStopActivity()
ActivityThread.performStopActivityInner()

总结上述app启动流程

1.点击app的启动图标时,Launcher进程通过Binder 向System_Server(AMS)发起startActivity请求,

2.System_Server进程(AMS)接受请求后,通过Socket方式向Zygote进程发送创建进程请求,然后Zygote进程通过Socket方式fork创建出一个新的APP进程分配给该应用;(只有和Zygote进程相关的用到了Socket通信)

为什么Zygote通信为什么用Socket,而不是Binder?

先后时序问题:
虽然Zygote更晚创建,但是不能保证通过Zygote进程去注册binder的时候,ServiceManager已经初始化好了(参考Binder C/S原理)。注册时间点无法保证,AMS无法获取到Zygote的binder引用,这是原因。

(附上开机到Launcher启动图)

3.APP进程通过Binder向System_Server(AMS)发起attachApplication请求,然后System_Server(AMS)再返回来给App进程发送scheduleLaunchActivity请求,告诉ActivityThread通过反射方式启动main方法,开启消息循环loop;以及告诉ActivityThread##H(Handler) 来反射启动创建Application 实例同时回调onCreate等方法,之后依次创建和初始化Application类,调用performLaunchActivity创建MainActivity类,加载主题样式Theme中背景等属性;然后再inflate布局,当oncreate/onstart/onresume方法都走完了之后,最后才对contentview进行measure/layout/draw显示再界面上,到此,应用的第一次启动才算完成;

4. 同时Laucher进程也会调用onPause暂停

Application构造器方法--->Application中attachBaseContext()--->Application中onCreate()--->Activity构造方法--->onCreate()--->配置主题中背景等属性——>onStart()——>onResume()——>测量布局绘制显示在界面上。

在onCreate中设置layout布局 创建window,在onResume中关联window测量绘制布局并显示window;

oncreate创建window,onresume显示window;

Window创建流程:


onCreate: 在上面调用了handlerLanchActivity之后,在创建Activity的同时并在里面新建了一个PhoneWindow对象和DecorView对象同时获取WindowManager,WindowManger负责和WMS进行Binder通信onResume: 调用了Activity.makeVisible()后会创建一个ViewRootImpl,然后将Decorview传入并被ViewRootImpl管理,比如DecorView的添加移除等view逻辑事件操作都是ViewRootImpl管理;ViewRootImpl实现了ViewParent这个接口,接口中常见方法requestLayout(),其实是调用了ViewRootImpl;所以当View的ViewParent都是同一个时,可理解为这些View都在一个View链上,invalidate的时候会将整个viewtree遍历刷新一遍;

优化应用启动时间:

1.在Application类中的构造器/attachBaseContext/onCreate方法中不要进行耗时操作;

2.如果在Activity类中对sp进行初始化,由于sp的特性会把数据全部读出来并放在内存里,所以大文件的sp初始化可以放在异步线程中处理而不放在主线程中;

3.对于Activity类初始化,因为计算启动时间是到获取第一帧截至,所以尽量减少布局层次性,以及onCreate、onStart、onResume方法中避免做耗时操作;

使用ActivityManager API时调用AMS服务的原理:

Android中Activity Manager相关类继承层次关系

       看一下类结构图如下:    

  IActivityManager作为ActivityManagerProxy和ActivityManagerNative的公共接口,所以两个类具有部分相同的接口,可以实现合理的代理模式;ActivityManagerProxy代理类是ActivityManagerNative的内部类;ActivityManagerNative是个抽象类,真正发挥作用的是它的子类ActivityManagerService(系统Service组件)

这里涉及两个过程

  代理对象建立:ActivityManagerProxy代理对象的创建;

  程序执行过程:如何通过代理对象来执行真正对象请求;

  从图中可以看出代理类:使用ActivityManagerProxy代理类,来代理ActivityManagerNative类的子类ActivityManagerService;ActivityManagerService是系统统一的Service,运行在独立的进程中;通过系统ServiceManger获取;

  ActivityManager运行在一个进程里面,ActivityManagerService运行在另一个进程内,

对象在不同的进程里面,其地址是相互独立的;实现跨进程的对象访问,需要对应进程间通信的规则,此处是采用Binder机制实现跨进程通信;所以此处的Proxy模式的运用属于:远程代理(RemoteProxy)

代理实现过程

1 代理对象建立

       是在ActivityManager的getRunningServices执行时就需要代理类来执行;

  public List<RunningServiceInfo> getRunningServices(int maxNum)

    return ActivityManagerNative.getDefault()

      getServices(maxNum, 0);

  }

  继续看看ActivityManagerNative.getDefault()到底干了什么事:

  实际上是关乎到Singleton<IActivityManager>类型的gDefault对象创建;

private static final Singleton<IActivityManager> gDefault = new
       Singleton<IActivityManager>() {
      protected IActivityManager create() {
      IBinder b = ServiceManager.getService("activity");
      IActivityManager am = asInterface(b);
      return am;
  }
};

ServiceManager.getService("activity");获取系统的“activity”的Service, 

所有的Service都是注册到ServiceManager进行统一管理。

  这样就创建了一个对ActivityManagerService实例的本地代理对象ActivityManagerProxy实例。Singleton是通用的单例模板类。

       ActivityManagerNative.getDefault就返回一个此代理对象的公共接口IActivityManager类型,就可以在本地调用远程对象的操作方法。

2 执行过程

       这个执行过程就设计到ActivityManager框架的执行流程;简单看一下这个getServices的执行过程。

  此图表明整个Client对Service的访问是通过Service的代理对象Proxy进行访问的。

Android中对Service访问的模式都是以Client/Server模式进行;Client实际上访问Service是通过对Service的建立代理的Proxy对象进行访问的——代理模式。

Android Binder

Binder优点:

1.性能方面

 Binder拷贝数据需要一次,而传统的Linux/管道等IPC方式需要拷贝两次数据,虽然共享内存方式不需要进行数据拷贝,但是实现方式比较复杂;

进程通信方式:

Linux的进程间通信的方式: 管道/socket/消息队列,Binder,共享内存;

2.安全方面:

由于传统的IPC接收方无法获得对方进程的UID/PID,无法鉴别对方身份;Android中为每一个应用程序分配了单独的UID,所以Binder是相对更安全的

Binder总体架构:

前提知识:

一个进程空间分为了 用户空间和内核空间

进程间,用户空间数据不能共享,内核空间数据可共享

Binder内部结构:

Client进程:

使用服务的进程,类似于Android客户端;

Server进程:

提供服务的进程,类似服务端;

ServiceManager进程: 

注册与查询服务作用,类似路由器;

Binder驱动:

一种虚拟设备驱动,链接Server进程,Client进程,ServiceManger的桥梁

1.传递进程间的数据:

a.当Client端向Server端发起IPC请求,Client端把请求数据从用户空间拷贝到内核空间;

b.Binder驱动将内核空间的数据拷贝到Server端所在的用户空间;

2.实现线程管理:采用Binder的线程池,并由Binder驱动自己进行管理;

Binder通信流程:


流程说明:

1.注册服务:

a.Server进程向Binder驱动发起服务注册请求

b.Binder驱动将注册请求转发给ServiceManager进程

c.ServiceManager进程帮Server进程注册成功,此时ServiceManager进程将获取到Server进程信息;

2.获取服务:

a.Client向Binder驱动发起获取服务请求,传递服务的名称;

b.Binder驱动将请求转发给ServiceManager进程

c.ServiceManager进程根据服务名称找到Client需要的服务信息;

d.Binder驱动将查询到的服务信息从Server端返回给Client端,此时Client和Server已经建立连接;

3.使用服务:

.Client将参数数据传给传给Server进程

a.Client进程将传送数据放入到共享内存中;

b.Binder驱动从Client共享内存中读取数据,根据ServiceManager进程找到对应的Server进程,将数据拷贝到Server进程的共享内存中进行解包;

二.Server进程根据Client进程要求调用目标方法

a.收到Binder驱动通知后,Server从线程池中取出线程,进行数据解包&调用目标方法,最终执行结果写入Server的共享内存中

三.Server将结果返回Client

a.Binder将Server共享内存中的结果拷贝到Client共享内存中;

上述流程中的额外说明:
1.Binder驱动和ServiceManager进程属于Android基础架构(系统已经实现好了的),Client和Server属于Android应用层(开发者自己实现);2.Binder的线程管理:Server会创建很多线程来处理Binder的请求,而这些线程是属于Binder线程池的,且归Binder自身管理,默认线程最大数量是16;

Android BroadCast底层原理:

Android中广播的基本原理,具体实现流程要点粗略概括如下:

1.广播接收者BroadcastReceiver通过Binder机制向AMS(Activity Manager Service)进行注册;

2.广播发送者通过binder机制向AMS发送广播;

3.AMS查找符合相应条件(IntentFilter/Permission等)的BroadcastReceiver,将广播发送到BroadcastReceiver(一般情况下是Activity)相应的消息循环队列中;

4.消息循环执行拿到此广播,回调BroadcastReceiver中的onReceive()方法。

 对于不同的广播类型,以及不同的BroadcastReceiver注册方式,具体实现上会有不同。但总体流程大致如上。

由此看来,广播发送者和广播接收者分别属于观察者模式中的消息发布和订阅两端,AMS属于中间的处理中心。广播发送者和广播接收者的执行是异步的,发出去的广播不会关心有无接收者接收,也不确定接收者到底是何时才能接收到

View的绘制流程:

先顺带提一下LayoutInflater:

layoutInflater.inflate(resourceId, root,attachToRoot):

 resourceId传入要加载的xml布局;root如果不为空,就相当于给这个布局外部嵌套一层root父布局;attachToRoot在两个参数的时候默认为true,如果设为false,则会将布局文件最外层的所有layout属性进行设置,当该view被添加到父view当中时,这些layout属性会自动生效。

在inflate方法中,采用了PULL解析方法来解析XML,首先会创建XmlPullParser对象并通过next()方法不断地遍历节点直到遍历完全部节点,根据解析出来的节点名来创建View对象(通过createViewFromTag这个api);

绘制流程:

在onResume执行后,会创建一个ViewRootImpl然后将Decorview交给它进行管理,DecorView底下的view逻辑事件都是ViewRootImpl来管理;ViewRootImpl实现了ViewParent接口,包含了requestLayout方法(内部包含了两步,检查是否是UI线程和调用performTraversals方法),所以当View的ViewParent都是同一个,可以理解为这些View都在View链上,invalidate的时候会将整个Viewtree刷新一遍;   View的绘制流程是从ViewRootImpl的performTraversals方法开始,依次调用measure。layout,draw,将整个Viewtree进行遍历执行这三个方法

1.OnMeasure:测量view的宽/高

2.OnLayout:决定view的四个顶点位置,以及拿到View的实际宽/高

3.OnDraw:绘制view

OnMeasure:


其中最重要的参数MeasureSpec,是一个32位int型,高2位是specMode,低30位是specSize;

通过父类的measureSpec,由specSize和specMode共同组成的,specSize来获取子类的size;

三种specMode:

EXACTLY:精准大小,表示父视图希望子视图大小由specSize决定或者由开发人员自己设置任意某个数值

AT_MOST:不超过父类;表示子视图最大只能小于specSize指定的数值,系统默认按照这个规则设置,开发人员也可以自己设置任意小于specSize数值;

UNSPECIFIED:可将试图按照自己意愿设置成任意大小;

内部是通过判断传入xml中的参数来产生specMode:

Match_Parent或者具体数值   --->   EXACTLY

WRAP_CONTENT  ---> AT_MOST

onMeasure方法最后会在getDefaultSize中通过判断specMode来拿到具体size:

 但是注意一点的是:AT_MOST它是和EXACTLY一样,都会返回父View规定的size;如果开发者不想在使用WRAP_CONTENT时展现出和MATCH_PARENT的样子,那么就可以在onMeasure中自己拿到specMode进行判断和设置,比如设一个具体的数值(如下);

在拿到实际要设置的size大小后,如下图调用setMeasuredDimension(width,height),这样一次measure过程就结束了;

常见用法:

当然上面只是view的onMeasure流程,如果存在一个父容器ViewGroup包含了多个子view,ViewGroup中定义了measureChildren()方法遍历测量包含的子view,每个子view的measure流程和上述view的流程相同

onLayout:


layout()方法中接受了左上右下四个坐标位置,并用setFrame和onLayout()方法来确定view的位置;

然而View中onLayout是一个空方法,因为onLayout()是为了确定view在布局中所在的位置,所以应该由父容器来决定;所以只有ViewGroup像LinearLayout等中才实现了onLayout方法用来约束子view的位置;

用法举例:

1.先去measure判断是否包含子视图,有的话就量出它的大小;

2.layout方法中传入的四个参数依次是0、0、childView.getMeasuredWidth()和childView.getMeasuredHeight(),其中,调用childView.getMeasuredWidth()和childView.getMeasuredHeight()方法得到的值就是在onMeasure()方法中测量出的宽和高

问题:getMeasureWidth和getWidth区别?

getMeasureWidth拿到的是onMeasure时setMeasuredDimension方法进行设置的值, 在measure()过程结束后获取到;

getWidth是在onLayout时传入的右边界减去左边界的值, 要在layout()过程结束后获取到;

所以如果处理了setMeasuredDimension和onLayout传左右边界的值,就可能会让getMeasureWidth和getWidth不同;

onDraw

1.drawBackground   绘制 View 的背景。

2.onDraw  绘制View的内容,根据自己需求进行绘制;

3.dispatchDraw  遍历子View进行绘制内容;

JSBridge原理:

JsBridge是Android Native原生和H5之间通信的桥梁,而且这种通信是双方互通的

H5调用Native方法:


1.从H5页面通过JSBridge触发一个url scheme;   (url scheme具体作用:可以用系统的OpenURI打开一个类似于url的链接(可拼入参数),然后系统会进行判断,如果是系统的url scheme,则打开系统应用,否则找看是否有app注册这种scheme,打开对应app);

2.Native页面捕获对应的url scheme,并对其进行分析拿到是属于哪一种回调函数的id后执行该方法;

3.执行完毕后通知H5进行回调通知它自己销毁该方法,否则可能引发内存泄漏;

 
Native调用H5方法:


1.Native通过JSbridge调用JS本地方法;
2.H5页面检测到方法调用并通过Native传来的api名称和参数判断是否允许调用,如果可以则执行方法;如果不可,h5页面上会有对应的错误提示。

Glide源码:

常见用法:

进阶点的:

Glide缓存:

简单介绍

Glide的缓存机制,主要分为2种缓存,一种是内存缓存(分为Lru算法的缓存和弱引用缓存),一种是磁盘缓存。之所以使用内存缓存的原因是:防止应用重复将图片读入到内存,造成内存资源浪费。之所以使用磁盘缓存的原因是:防止应用重复的从网络下载和读取数据;

详细介绍

读取一张图片的时候,获取顺序:Lru算法缓存-》弱引用缓存-》磁盘缓存(如果设置了的话)。

当我们的APP中想要加载某张图片时,先去LruCache中寻找图片,如果LruCache中有,则直接取出来使用,并将该图片放入WeakReference中,如果LruCache中没有,则去WeakReference中寻找,如果WeakReference中有,则从WeakReference中取出图片使用,如果WeakReference中也没有图片,则从磁盘缓存/网络中加载图片。 

将图片缓存的时候,写入顺序:弱引用缓存-》Lru算法缓存-》磁盘缓存中。

当图片不存在的时候,先从网络下载图片,然后将图片存入弱引用中,glide会采用一个acquired(int)变量用来记录图片被引用的次数, 当acquired变量大于0的时候,说明图片正在使用中,也就是将图片放到弱引用缓存当中;如果acquired变量等于0了,说明图片已经不再被使用了,那么此时会调用方法来释放资源,首先会将缓存图片从弱引用中移除,然后再将它put到LruResourceCache当中。这样也就实现了正在使用中的图片使用弱引用来进行缓存,不在使用中的图片使用LruCache来进行缓存的功能。

引申一下LruCache:

最近最少使用算法,设定一个缓存大小,当缓存达到这个大小之后,会将最老的数据移除,避免图片占用内存过大导致OOM。LruCache 内部用LinkHashMap存取数据,在双向链表保证数据新旧顺序的前提下,设置一个最大内存,往里面put数据的时候,当数据达到最大内存的时候,将最老的数据移除掉,保证内存不超过设定的最大值。

关于LinkedHashMap:

LinkHashMap继承HashMap,在 HashMap的基础上,新增了双向链表结构,每次访问数据的时候,会更新被访问的数据的链表指针,具体就是先在链表中删除该节点,然后添加到链表头header之前,这样就保证了链表头header节点之前的数据都是最近访问的(从链表中删除并不是真的删除数据,只是移动链表指针,数据本身在map中的位置是不变的)。

关于磁盘缓存

DiskLruCache 跟 LruCache 实现思路是差不多的,一样是设置一个总大小,每次往硬盘写文件,总大小超过阈值,就会将旧的文件删除;

Q0:假如自定义图片加载框架要考虑哪些问题?

异步加载:线程池
切换线程:Handler,没有争议吧
缓存:LruCache、DiskLruCache
防止OOM:软引用、LruCache、图片压缩、Bitmap像素存储位置
内存泄露:注意ImageView的正确引用,生命周期管理
列表滑动加载的问题:加载错乱、队满任务过多问题
详细计划:

1.由于采用三级缓存策略:内存缓存,磁盘,网络; 所以如下分析要三个线程池差不多; 

2.切换线程刷新UI;

3.内存缓存:LruCache;磁盘缓存;网络(上面分析过就不展开分析了)

4.防止OOM:LruCache会移除最近一直没用的对象保证不会OOM,但如果图片单张过大,可以考虑降低图片bitmap格式,不采用argb8888而是采用rgb565;

5.listview列表异步加载图片错乱问题:给view设置tag;

Q1:PICASSO和Glide的区别:

Glide:
图片的异步加载(基础功能)

支持设置加载尺寸、设置加载中以及加载失败图片、设置加载动画、设置图文混排

多样式的媒体加载(设置缩略图支持:先加载缩略图 然后在加载全图)

支持设置磁盘缓存策略、设置跳过内存缓存、清理缓存

通过设置绑定生命周期,更好的让加载图片的请求的生命周期动态管理起来;

优点:

支持多样化媒体加载
Glide 不仅是一个图片缓存,它支持 Gif、WebP、缩略图,甚至是 Video
生命周期集成
通过设置绑定生命周期,我们可以更加高效的使用Glide提供的方式进行绑定,这样可以更好的让加载图片的请求的生命周期动态管理起来(使用还是有bug)
高效的缓存策略
1.支持Memory和Disk图片缓存
2.Picasso 只会缓存原始尺寸的图片,而 Glide 缓存的是多种规格,即 Glide 会根据你 ImageView 的大小来缓存相应大小的图片尺寸
3.内存开销小,Glide 默认的 Bitmap 格式是 RGB_565 格式,1像素占用 2 byte,而 Picasso 默认的是 ARGB_8888 格式,1像素占用 4 byte,占用内存要小一半,OOM概率会小一些;
缺点:使用方法复杂,实现方法较多

Picasso:
图片的异步加载(最基础功能)

使用最少的内存完成复杂的图片转换,转换图片以适合所显示的ImageView,来减少内存消耗

支持加载过程中和加载错误时显示对应图片

在Adapter中的回收不在视野的ImageView和取消已经回收的ImageView下载进程

支持加载多种数据源 网络、本地、资源、Assets 等

自动添加磁盘和内存二级缓存功能

支持优先级处理

支持飞行模式、并发线程数根据网络类型而变

优点:使用简单,代码简洁

缺点:

功能较为简单-图片加载;
自身无实现“本地缓存”


Q2:Glide加载一个一兆的图片(100*100),是否会压缩后再加载,放到一个200*200的view上会怎样,1000*1000呢,图片会很模糊,怎么处理?

当我们调整imageview的大小时,Picasso会不管imageview大小是什么,总是直接缓存整张图片,而Glide就不一样了,它会为每个不同尺寸的Imageview缓存一张图片,也就是说不管你的这张图片有没有加载过,只要imageview的尺寸不一样,那么Glide就会重新加载一次,这时候,它会在加载的imageview之前从网络上重新下载,然后再缓存。举个例子,如果一个页面的imageview是300 * 300像素,而另一个页面中的imageview是100 * 100像素,这时候想要让两个imageview像是同一张图片,那么Glide需要下载两次图片,并且缓存两张图片。

Q3:简单说一下内存泄漏的场景,如果在一个页面中使用Glide加载了一张图片,图片正在获取中,如果突然关闭页面,这个页面会造成内存泄漏吗?

因为Glide 在加载资源的时候,如果是在 Activity、Fragment 这一类有生命周期的组件上进行的话,会创建一个透明的 RequestManagerFragment 加入到FragmentManager 之中,感知生命周期,当 Activity、Fragment 等组件进入不可见,或者已经销毁的时候,Glide 会停止加载资源。但是如果,是在非生命周期的组件上进行时,会采用Application 的生命周期贯穿整个应用,所以 applicationManager 只有在应用程序关闭的时候终止加载。

okhttp3源码:

使用同步的okhttp举例:

使用异步的Okhttp举例:

源码分析:

简单介绍

不管是同步还是异步请求,都是通过client.newCall(request)来执行,newCall其实是创建了一个RealCall对象。在RealCall执行之前会检查是否已经执行过,由于一个Call对象只能执行一次,执行过则报异常;真正进行网络请求的地方是getResponseWithInterceptorChain()方法,该方法通过一系列拦截器构成拦截链,链式执行proceed()方法拿到Response完成网络请求;

详细介绍

创建OkHttpClient对象


上面采用了Builder模式来获取OKHttpClient对象,(Request和Response也用了);

建造者好处:

使用建造者模式可以使客户端不必知道产品内部的组成细节。(封装性)

具体的建造者之间是相互独立的,对系统的扩展非常有利。(扩展性)

 创建Request对象:

采用异步方法enqueue:

 看看最重要的分发器Dispatcher(线程池):

Dispatcher 的功能:

  • 记录同步任务、异步任务及等待执行的异步任务。
  • 调度线程池管理异步任务。
  • 发起/取消网络请求 API:execute、enqueue、cancel。

采用同步请求excute():

okhttpclient对象调用newCall方法,从而创建一个call对象;

 下面是同步方法execute:

真正进行网络请求的地方:getResponseWithInterceptorChain:

 分析完同步的execute和异步的enqueue请求方法,再分析一下获取response的过程:采用了责任链模式获取response;

用这种模式的好处是:将请求的发送和处理分开,并且可以动态添加中间的处理方实现对请求的处理、短路(直接获取缓存或者网络请求)等操作

其中比较重要的几个拦截器:

RetryAndFollowUpInterceptor:重试和失败重定向拦截器

BridgeInterceptor:桥接拦截器,处理一些必须的请求头信息的拦截器

CacheInterceptor:缓存拦截器,用于处理缓存

ConnectInterceptor:连接拦截器,建立可用的连接,是CallServerInterceptor的基本

CallServerInterceptor:请求服务器拦截器将 http 请求写进 IO 流当中,并且从 IO 流中读取响应 Response;

Reponse从CacheInterceptor中的缓存或者CallServerInterceptor网络请求中获取到

Q0:okhttp运用的设计模式

  • 构造者模式(OkhttpClient,Request等各种对象的创建)
  • 责任链模式(拦截器的链式调用)
  • 策略模式(在CacheInterceptor中,在响应数据的选择中使用了策略模式,选择缓存数据还是选择网络访问。
  • 享元模式(Dispatcher的线程池中,不限量的线程池实现了对象复用)
  • 工厂模式(在Call接口中,有一个内部工厂Factory接口。)
  • 单例模式(Platform类,已经使用Okhttp时使用单例)

Q1:okhttp有哪些优势(对于网络请求都有哪些优化)  缺点?

1)支持http2,对一台机器的所有请求共享同一个socket

2)内置连接池,支持连接复用,减少延迟

HttpEngine 在发起请求之前,会先调用nextConnection()来获取一个Connection对象,如果可以从ConnectionPool中获取一个Connection对象,就不会新建,如果无法获取,就会调用createnextConnection()来新建一个Connection对象,这就是 Okhttp 多路复用的核心,不像之前的网络框架,无论有没有,都会新建Connection对象;

3)支持透明的gzip压缩响应体:

GZIP是网站压缩加速的一种技术,对于开启后可以加快我们网站的打开速度,原理是经过服务器压缩,客户端浏览器快速解压的原理,可以大大减少了网站的流量

开GZIP有什么好处?Gzip开启以后会将输出到用户浏览器的数据进行压缩的处理,这样就会减小通过网络传输的数据量,提高浏览器启动页面的速度。

4)通过缓存避免重复的请求

OKHttp 默认只支持 get 请求的缓存。

第一次拿到响应后根据头信息决定是否缓存。
下次请求时判断是否存在本地缓存,是否需要使用对比缓存、封装请求头信息等等。
如果缓存失效或者需要对比缓存则发出网络请求,否则使用本地缓存。
5)请求失败时自动重试主机的其他ip,自动重定向

6)丰富的API,可扩展性好

缺点:比如callback回来是在线程里面, 不能刷新UI,需要我们手动处理。

Q2:okhttp实现网络请求的方法

OkHttp3的最底层是Socket,而不是URLConnection,它通过Platform的Class.forName()反射获得当前Runtime使用的socket库

socket发起网络请求的流程一般是:

(1). 创建socket对象;

(2). 连接到目标网络;

(3). 进行输入输出流操作。

(1)(2)的实现,封装在connection接口中,具体的实现类是RealConnection。(3)是通过stream接口来实现,根据不同的网络协议,有Http1xStream和Http2xStream两个实现类,由于创建网络连接的时间较久(如果是HTTP的话,需要进行三次握手),而请求经常是频繁的碎片化的,所以为了提高网络连接的效率,OKHttp3实现了网络连接复用

OkHttp 的底层是通过 Socket 发送 HTTP 请求与接受响应,但是 OkHttp 实现了连接池的概念,即对于同一主机的多个请求,可以公用一个 Socket 连接,而不是每次发送完 HTTP 请求就关闭底层的 Socket,这样就实现了连接池的概念。而 OkHttp 对 Socket 的读写操作使用的 OkIo 库进行了一层封装。

Q3:如何拿OkHttp实现断点续传?

step 1:判断检查本地是否有下载文件,若存在,则获取已下载的文件大小 downloadLength,若不存在,那么本地已下载文件的长度为 0
step 2:获取将要下载的文件总大小(HTTP 响应头部的 content-Length)
step 3:比对已下载文件大小和将要下载的文件总大小(contentLength),判断要下载的长度
step 4:再即将发起下载请求的 HTTP 头部中添加即将下载的文件大小范围(Range: bytes = downloadLength - contentLength)

可参考博文:https://blog.csdn.net/emmmsuperdan/article/details/82150004

Q4:Volley网络请求框架和Okhttp有什么区别?

volley缺点: 不支持同步,这点会限制开发模式且不适合大文件上传和下载,Volley自己的定位是轻量级网络交互,适合大量的,小数据传输。
Volley在Android 2.3及以上版本,使用的是HttpURLConnection,而在Android 2.2及以下版本,使用的是HttpClient,不过在android6.0不支持httpclient了。
不过再怎么封装Volley在功能拓展性上始终无法与OkHttp相比。Volley停止了更新,而OkHttp得到了官方的认可,并在不断优化。

Q5:为什么response.body().string() 只能调用一次

我们可能习惯在获取到Response对象后,先response.body().string()打印一遍log,再进行数据解析,却发现第二次直接抛异常,其实直接跟源码进去看就发现,通过source拿到字节流以后,直接给closeQuietly悄悄关闭了,这样第二次再去通过source读取就直接流已关闭的异常了。  public final String string() throws IOException {
    BufferedSource source = source();
    try {
      Charset charset = Util.bomAwareCharset(source, charset());
      return source.readString(charset);
    } finally {
      //这里讲resource给悄悄close了
      Util.closeQuietly(source);
    }
  }
解决方案:1.内存缓存一份response.body().string();2.自定义拦截器处理log。

Android APK打包流程

流程:

1. 打包资源文件,生成R.java文件

打包资源的工具是aapt,在这个过程中,项目中的AndroidManifest.xml文件和布局文件XML都会编译,然后生成相应的R.java,存放在APP的res目录下的资源,该类资源在APP打包前大多会被编译,变成二进制文件,并会为每个该类文件赋予一个resource id。对于该类资源的访问,应用层代码则是通过resource id进行访问的。Android应用在编译过程中aapt工具会对资源文件进行编译,并生成一个resource.arsc文件,resource.arsc文件相当于一个文件索引表,记录了很多跟资源相关的信息;

2. 处理aidl文件,生成相应的Java文件

这一过程中使用到的工具是aidl,aidl工具解析接口定义文件然后生成相应的Java代码接口供程序调用。如果在项目没有使用到aidl文件,则可以跳过这一步。

3. 编译项目源代码,生成class文件

项目中所有的Java代码,包括R.java和.aidl文件,都会变Java编译器(javac)编译成.class文件,生成的class文件位于工程中的bin/classes目录下。

4. 转换所有的class文件,生成classes.dex文件

dx工具生成可供Android系统Dalvik虚拟机执行的classes.dex文件。dx工具的主要工作是将Java字节码转成成Dalvik字节码、压缩常量池、消除冗余信息等。

5. 打包生成APK文件

所有没有编译的资源,如images、assets目录下资源(该类文件是一些原始文件,APP打包时并不会对其进行编译,而是直接打包到APP中,对于这一类资源文件的访问,应用层代码需要通过文件名对其进行访问);编译过的资源和.dex文件都会被apkbuilder工具打包到最终的.apk文件中

6. 对APK文件进行签名

一旦APK文件生成,它必须被签名才能被安装在设备上。

在开发过程中,主要用到的就是两种签名的keystore。一种是用于调试的debug.keystore,它主要用于调试,在Eclipse或者Android Studio中直接run以后跑在手机上的就是使用的debug.keystore。

另一种就是用于发布正式版本的keystore。

HTTP相关以及加密流程

HTTP1.0和HTTP1.1区别:

1.HTTP1.1支持长连接,在一个TCP连接上可以进行多次请求和响应,减少了建立和关闭连接的消耗和延迟(Connection: Keep-Alive);而HTTP1.0每次请求都需要重新创建连接;

2.带宽优化:HTTP1.1在请求头引入了range头域,允许只请求资源的某一部分,实现断点续传功能优化带宽;而HTTP1.0每次请求都从文件头开始传输整个数据资源;

通过SPDY方案对http1.x进行了优化,并衍生出HTTP2.0。两者区别:HTTP2.0 支持明文 HTTP 传输,而 SPDY强制使用 HTTPS;

HTTP2.0和HTTP1.x比较:

1.  2.0采用二进制格式,而HTTP1.x是基于文本进行解析;文本具有多样性因此需要考虑的场景很多

2. 2.0多路复用: 建立的连接中可以同时携带多个request,接收方根据id再归属到不同的服务端请求中;

3. 2.0head头部压缩:减小传输大小

HTTP和HTTPS区别:

1.HTTP(超文本传输协议)运行在TCP上,传输的内容都是明文;HTTPS(超文本传输安全协议)运行在SSL(依靠证书来验证服务器身份并进行通信加密)/TLS之上,然后再运行再TCP上,传输内容都属经过加密的;

2.此外,HTTPS用的是443端口,HTTP用的是80端口

HTTP报文(请求和响应):

请求报文:

组成

请求行(含请求方法GET或POST等,以及URL  (Uri一般用来定位本地文件,URL一般用来网络请求))),请求头部,请求数据;

请求方法:

GET和POST区别:

1.GET可以提交少量参数,放在URL之后用?分开,参数和参数间用&连接,eg:baidu.com?name=test1&id=123456;  而POST会把提交的数据放到请求数据中;

2.GET提交数据有大小限制(因为浏览器对URL有长度限制,1024个字符),而POST方法提交的数据没有限制;

3.GET方式提交关键信息如用户名密码等会出现在URL中 并不安全;

请求头部:

含若干个属性,格式为“属性名:属性值”,服务端据此获取客户端的信息。例如:

Accept:浏览器能接受的媒体类型;

Accept-language:可接受的语言 

Cookie:用来存储一些用户信息以便让服务器辨别用户身份的(cookie是存储在本地浏览器,而session存储在服务器,是一种服务器端的机制,服务器使用一种类似于hastable的结构来保存信息);

请求数据:

GET中没有请求数据而POST中有,POST适用于客户填写表单场合;

响应报文:

组成

状态行(含响应码),首部行(响应结果的长度,响应结果的类型等等),实体主体(响应正文);

响应码:

被包含在状态行中,常见响应码有:

200 OK:客户端请求成功。
400 Bad Request:客户端请求有语法错误,不能被服务器所理解。
401 Unauthorized:请求未经授权,这个状态代码必须和WWW-Authenticate报头域一起使用。
403 Forbidden:服务器收到请求,但是拒绝提供服务。
404 Not Found:请求资源不存在,举个例子:输入了错误的URL。
500 Internal Server Error:服务器发生不可预期的错误。
503 Server Unavailable:服务器当前不能处理客户端的请求,一段时间后可能恢复正常,举个例子:HTTP/1.1 200 OK(CRLF)。

HTTP工作流程:

     1 ) 、地址解析   

如用客户端浏览器请求这个页面:http://localhost.com:8080/index.htm

  在这一步,需要域名系统DNS解析域名localhost.com,得主机的IP地址(因为用的是TCP/IP协议,所以要拿IP)。
    2)、封装HTTP请求数据包

     把以上部分结合本机自己的信息,封装成一个HTTP请求数据包
     3)封装成TCP包,建立TCP连接(TCP的三次握手)

       在HTTP工作开始之前,客户端首先要通过网络与服务端连接,通过TCP来完成的,该协议与IP协议共同构建Internet,即著名的TCP/IP协议族,因此Internet又被称作是TCP/IP网络。

     4)客户机发送请求命令

       建立连接后,客户机发送一个HTTP请求响应给服务器

     5)服务器响应     

     服务器接到请求后,返回HTTP响应信息

     6)服务器关闭TCP连接(四次挥手)

     一般情况下,一旦Web服务器向浏览器发送了请求数据,它就要关闭TCP连接,然后如果浏览器或者服务器在其头信息加入了这行代码 Connection:keep-alive 就代表需要保持连接不断开;

     7)浏览器渲染

对称和非对称加密

对称加密

加密与解密使用的是同样的共享密钥,所以速度快,但由于需要将密钥在网络传输,容易导致密钥泄漏,难以保证消息可靠性。

非对称加密

采用非对称时,客户端和服务端均拥有一个公钥和私钥,公钥对外开放,私钥只有自己方可见,公钥加密只有私钥才能解密;这样,客户端在发送消息前先用服务端公钥加密,消息到了服务端,服务端自己再用私钥解密;   优点是安全,缺点:比对称加密慢,其次如果客户端发送的过程中公钥被中间人替换了,这样服务端无法确认公钥正确性;

由于对称和非对称都有缺点,所以HTTPS采用数字证书(CA)的方式进行加密

数字签名(SSL)和数字证书

一般是服务端主动去向CA(认证中心)申请数字证书,步骤:

1.本地生成一对公私钥,然后拿着公钥去CA申请加密;

2.CA拿到公钥,进行单向HASH算法加密(常见如MD5算法)得到摘要;

3.CA用私钥对摘要进行加密得到数字签名;

4.摘要和数字签名一起就称为数字证书(SSL),最后传给客户端;

数字证书如何在客户端起作用?

电脑和浏览器(客户端)用CA的公匙解密数字证书,如果解密成功则说明证书来源于合法的认证机构。解密成功后,客户端就拿到了摘要。

此时,客户端会按照和CA一样的Hash算法将申请信息生成一份摘要,并和解密出来的那份做对比,如果相同则说明内容完整,没有被篡改。最后,客户端安全的从证书中拿到服务器的公匙就可以和服务器进行安全的非对称加密通信了。服务器想获得客户端的公匙也可以通过相同方式。

TCP和UDP

TCP和UDP区别:

1.TCP面向连接(三次握手建立);UDP发数据前无需建立连接

2.TCP可靠传输(原理在下面);UDP不保证数据可靠性;

3.TCP面向字节流;UDP面向报文,无拥塞控制,数据发送速率高;

4.TCP只支持点到点;UDP支持多对多,一对多等通信;

TCP可靠传输原理:

1.通过确认和超时重传机制:

出现差错或丢失的时候,发送方会将自己备份的副本再重传一次,直到收到接收的确认信息;当接收方收到重复的数据时,会直接丢弃,但是会给发送方请确认自己已经收到了。

2.滑动窗口:

TCP 连接的每一端都必须设有两个窗口——一个发送窗口和一个接收窗口。
TCP 两端的四个窗口经常处于动态变化之中。
发送窗口:

  • 假设发送窗口是5,也就是发送方一次性能发5个数据包。当发送方收到数据包1的接收确认后表示接收方接收了数据包1,之后发送窗口向前滑动一个数据包,在发送窗口中删除数据包1的缓存。
  • 即如果发送了5个数据包后没有收到确认信息就会停止继续发送数据包。
  • 滑动窗口方式仍需每个数据包对应一个确认,效率不高,接收端可采用累积确认。

接收窗口:采用累积确认机制,发送方不对收到的分组逐个发送确认,而是对按序到达的最后一个分组发送确认,这样表示:到这个分组为止的所有分组都已正确收到了。

几个概念:

1. FIN: 请求关闭报文
2. SYN: 请求建立连接
3. ACK: 确认收到
4. MSL: 最大报文生存时间

三次握手:

第一步: 客户端想要与服务器建立连接, 于是向服务器发送SYN报文请求连接.

第二步: 服务器收到客户端的连接请求之后, 服务器向客户端发送确认报文ACK及请求连接报文SYN

第三步: 客户端收到服务器的连接请求, 向服务器发送确认报文ACK

为什么需要第三次客户端向服务端发送ACK确认请求呢?

如果只有两次握手的话,当出现网络阻塞时,客户端发送的连接请求迟迟到不了服务端,客户端便超时重发请求,如果服务端正确接收(第二次重传请求)并确认应答,双方便开始通信,通信结束后释放连接;这个之前被A认为”失效的消息“慢吞吞到达了B,而对于B而言,以为这是一个新的请求连接消息,就向A发了一次确认,而对于A而言,他认为他没有给B再次的发消息(上次发送完消息后进入closed状态),所以A不会理睬这条确认,但是B则会一直在等待着A的消息浪费资源;

四次挥手:


第一次挥手:客户端主动请求关闭,并向服务端发送FIN关闭请求;

第二次挥手:服务端收到并发送ACK确认告诉客户端已经收到关闭请求;

第三次挥手:服务端将之前需要发送的数据全部发送完毕后,主动向客户端发送FIN关闭请求;

第四次挥手:客户端收到并发送ACK确认请求,服务端收到ACK后直接关闭;而客户端是不知道服务器是否收到了这次ACK的,客户端这边需要再等待2MSL再进行关闭。如果2MSL内没有收到服务端重发的FIN,默认服务端收到ACK,则客户端关闭;

1. 为什么第二次跟第三次不能合并, 第二次和第三次之间的等待是什么?:

当服务器执行第二次挥手之后, 此时证明客户端不会再向服务器请求任何数据, 但是服务器可能还正在给客户端发送数据(可能是客户端上一次请求的资源还没有发送完毕), 所以此时服务器会等待把之前未传输完的数据传输完毕之后再发送关闭请求.

2. 主动发起方为什么最后要等待2MSL的时间:

上边我们说了服务器收到了最后一次的ACK报文之后就会关闭, 那客户端是不知道服务器是否收到了这次ACK的, 所以客户端只能在这等待, 如果服务器在"超时时间"内真的没有收到最后一次的ACK,就会重新发送一次FIN; 那么在网络极端情况下(网络没断, 但是速度极慢, 需要整整1MSL的时间才能把报文发送过去),这次重发FIN需要1MSL的时间, 这一来一回需要的时间总和为:服务器超时时间+1MSL, 那为了保险起见,直接让客户端等待2MSL的时间, 如果2MSL之内客户端没有收到重发的FIN, 则默认为服务器收到了最后一次ACK,此时客户端就可以执行关闭了.

网络协议分层

OSI七层协议模型

网络层是路由器,数据链路层是交换机,物理层是光纤,传输层是TCP/UDP协议! 

TCP/IP模型:

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值