瘟疫青年

一些知识点总结

Word count: 45.7kReading time: 168 min
2018/08/16 Share

博客好久没更新了,把最近总结的一些零星知识点汇总一下吧。

view 绘制流程相关:

如何在activity中正确获取view的宽高:

  1. activity/view onwindowFocusChanged()
  2. view.post(new Runnable{})
  3. ViewTreeObserver.addOnGlobalLayoutListener();
  4. 手动调用view.measure();

setWillNotDraw()

如果view不需要绘制任何内容,设置这个标记为true后系统会对其进行优化,默认View是没有启用的,但是viewGroup会默认启用,当我们开发的自定义控件继承viewGroup且本身不具备绘制功能,就可以开启该标记位便于系统优化。如果需要绘制内容,要显示关闭。

当子view的measureSpec的type是unspecified

子view的宽高是getSuggestMinWidth/Height 来获取的,如果有背景图,为背景图宽高,否则为布局制定的android:minWidth属性 (默认为0)

layout过程

自己的位置是由layout()方法决定,layout()中首先会调用setFrame() 来决定自己的上下左右位置,其次调用onLayout() 来确定子view的位置,不**同的viewGroup需要继承并自己实现其逻辑。

MotionEvent()

getX/getY getRawX getRawY 返回的分别为 相对于当前view左上角的x 和 y 以及相对于屏幕左上角的 x 和 y坐标。

view 的事件分发

  1. 当事件走到view这一层级的时候,首先会回调dispatchTouchEvent() –> 是否设置了onTouchListener? 设置了 –> 回调 onTouch() 并看onTouch的返回值 如果返回true -> 回调 onTouchEvent() 否则 不回调onTouchEvent()
  2. view 的onTouchEvent(),先判断是否可用,如果不可用,当view可点击(长按 短按都行)会默认消费掉事件,当view可用,通过event.getAction()判断当前事件类型,在actionUP的时候,调用performClick(),会判断是否调用OnClick()onLongClick().
  3. viewGroup决定拦截事件后,后续的事件都会交给她处理,不会再走onInterceptTouchEvent()方法,因为在调用onInterceptTouchEvent()之前会判断mFirstTouchTarget是否为空,如果viewGroup自己拦截处理,onInterceptTouchEvent = null 就进不到判断里,也就不走onInterceptTouchEvent()了。
  4. 当子view设disallowInterceptTouchEvent()之后,会修改父view的FLAG_DISALLOW_INTERCEPT 这个标记位,一旦设置之后,viewGroup就无法拦截down以后的事件了,但下次是还是可以收到down事件,因为在down事件来临的时候,viewGroup会重置这个标志位。也就是shidisallowInterceptTouchEvent()无法阻止父view 对down事件的处理。
  5. 当viewGroup不打算拦截,会将事件分发给子view,首先先判断哪些在触摸范围的子view,然后依次调用他们的dispatchTouchEvent
  6. 一般处理滑动冲突,最好用外部拦截法
    a. 重写父view的OnInterceptTouchEvent()
    b. 父容器的ACTION_DOWN必须返回false 如果返回true 后续事件将都有他处理,子view是收不到其他事件的
    c. ACTION_MOVE 要根据业务需求决定是否拦截
    d. ACTION_UP 必须返回false 因为这个事件作为事件结尾本身没有什么意义,而且如果返回true,子view将收不到up事件,onclick方法将无法触发。

requestLayout、invalidate与postInvalidate

requestLayout:

当前view将自己设置一个flag 同时调用父view的requestLayout,父view会设置一个标志位:PFLAG_FORCE_LAYOUT,这样逐级上调,直到decorView,decorView会吧view上报到viewRootImpl上,viewRootImp会调用 requestLayout(),依次触发子view的measure layout draw方法。

invalidata

invalidate有多个重载方法,但最终都会调用invalidateInternal方法,在这个方法内部,进行了一系列的判断,判断View是否需要重绘,接着为该View设置标记位,然后把需要重绘的区域传递给父容器,即调用父容器的invalidateChild方法。

在该方法内部,先设置当前视图的标记位,接着有一个do…while…循环,该循环的作用主要是不断向上回溯父容器,求得父容器和子View需要重绘的区域的并集(dirty)。当父容器不是ViewRootImpl的时候,调用的是ViewGroup的invalidateChildInParent方法,我们来看看这个方法,ViewGroup#invalidateChildInParent:

这个方法做的工作主要有:调用offset方法,把当前dirty区域的坐标转化为父容器中的坐标,接着调用union方法,把子dirty区域与父容器的区域求并集,换句话说,dirty区域变成父容器区域。最后返回当前视图的父容器,以便进行下一次循环。

回到上面所说的do…while…循环,由于不断向上调用父容器的方法,到最后会调用到ViewRootImpl的invalidateChildInParent方法
该方法所做的工作与上面的差不多,都进行了offset和union对坐标的调整,然后把dirty区域的信息保存在mDirty中,最后调用了scheduleTraversals方法,触发View的工作流程,由于没有添加measure和layout的标记位,因此measure、layout流程不会执行,而是直接从draw流程开始。

好了,现在总结一下invalidate方法,当子View调用了invalidate方法后,会为该View添加一个标记位,同时不断向父容器请求刷新,父容器通过计算得出自身需要重绘的区域,直到传递到ViewRootImpl中,最终触发performTraversals方法,进行开始View树重绘流程(只绘制需要重绘的视图)。

postinvalidata

发送了一个异步消息到主线程,显然这里发送的是MSG_INVALIDATE,即通知主线程刷新视图

activity的启动流程

  1. A activity启动B activity
  2. A 最终调用startActivityForResult()
  3. Instrumentation.exeStartActivity()
  4. ActivityManagerNative.getDefault() 通过binder机制 获得远端AMS的引用,在创建这个Binder对象时,传入了一个IBinder,其实是ServiceManager获得的,这个IBinder持有远端AMS服务的handle值,作为跟远AMS交流的信使。
  5. AMS.startActivity【此处有个分水岭 如果应用没有启动过 任务战中尚无待启动的应用程序 会走下面的流程 当应用是首次启动 会走22】
  6. ActivityStackSupervisor.startActivityMayWait()
  7. ActivityStackSupervisor.startActivityMayWait().startActivityLocked()
  8. startActivityLocked().startActivityUncheckedLocked()
  9. ActivityStack.resumeTopActivitiesLocked
  10. 回到ActivityStackSupervisor,调用到realStartActivityLocked()
  11. 通过binder机制 调用会ApplicationThread(本身是个binder类型,被AMS远程访问)的scheduleLaunchActivity()
  12. 发送一个启动activity的消息给handler H处理。
  13. H 收到之后,调用ActivityThreadhandleLaunchActivity()
  14. handleLaunchActivity() 中,调用performLaunchActivity 完成activity的创建
  15. performLaunchActivity中,通过ActivityClientRecord 获取启动的activity信息,通过InstrumentationnewActivity 使用类加载器创建activity,通过LoadApk 创建Application对象,其实还是通过Instrumentation通过类加载器创建的,随后会调用application类的onCreate方法。创建ContextImp对象,并调用activityattach方法,在attach方法中完成对window的创建,调用activityonCreate方法。
  16. attach中,创建了window接口的唯一实现类PhoneWindow对象,并将activity设置为window的回调接口(activity默认是实现了这些接口的),也就是我们在activity中可以复写的onAttachedToWindow,onDetachedToWindow之类。
  17. 回调activityonCreate()方法 执行setContentView()
  18. setContentView()实际是PhoneWindow执行的,首先创建顶级视图decorView 并把自定义布局添加到decorView中,此时该视图还没被添加到window中
  19. handleLaunchActivity之后会立刻回调onResume方法 会回调activitymakeVisible() 此时decorView 才真正被添加到PhoneWindow中,也就是执行了window.addView方法
  20. addView方法中,会创建viewRootImp 并将view添加到列表中,将viewRootImp添加到mRoots列表中
  21. 调用viewRootImpsetView() 在该方法内部会调用 requestLayout()开启异步刷新请求,通过scheduleTraversals()开始一次调用view的 onMeasure onLayout onDraw
  22. 注意viewRootImp的绘制方法 都是通过WMS 通过binder机制完成的。
  23. 如果应用尚未启动过,AMS的startActivity() 会调用zygoteSendAndGetResult() , 通过socket方式通知zygote进程为待启动应用fork一个新进程。
  24. fork新进程,其实就是创建了一个ActivityThread,并执行main()方法。
  25. 在目标activity的ActivityThread的main() 方法中,首先创建ActivityThread对象实例。
  26. 调用thread.attach() ,通过binder机制 通过AMS最终调用到ApplicationThread的bindApplication()方法。通过H handler发送了一H.BIND_APPLICATION的消息。
  27. 在attach方法内部 初始化了一个叫做H的handler 并开始looper.loop 轮训获取消息。
  28. 当H收到bindApplication的消息后,调用handleBindApplication()方法,通过loadAPK.makeApplication()来创建Application对象,此时application还是通过instrument对象通过反射创建的,创建完成之后调用application的oncreate,并创建ContextImp对象。
  29. 回到14

    A 的activity 中 通过Instrumentation.checkStartActivityResult() 检查启动是否正常,不正常会抛出相应异常

1
2
3
4
5
6
7
8
9
10
Application 构造函数
-> Application.attachBaseContext()
-> Application.onCreate()
-> Activity 构造函数
-> Activity.setTheme()
-> Activity.onCreate()
-> Activity.onStart
-> Activity.onResume
-> Activity.onAttachedToWindow
-> Activity.onWindowFocusChanged

如果要记录应用启动时间 在attachBaseContext()中打log 为启动开始时间
在activity的onWindoewFocusChanged() 打log记录结束时间,

热更新

  1. PathClassloaderDexClassLoader均继承自BaseDexClassLoader
  2. pathClassLoader只能加载安装包安装后解压路径下的dex文件,而DexClassLoader可以加载jar包,zip文件,APK文件中的dex文件,所以DexClassLoader肯定有一步解压操作,将压缩包的dex文件解压到制定目录,所以再构造函数中需要传入一个解压目录
  3. 在父类BaseDexClassLoaderloadClass()方法中,实际是通过dexPathListfindClass 来查找class的

  4. DexPathList的构造函数中,保存了当前的类加载器,同时将一个个的apk,dex,zip、jar之类的文件封装为一个个的Element,添加到Elements集合中,每个Element元素中都保存着对应的dex文件dexFile

  5. 封装Element的过程调用的makeDexElements()方法,用loadDexFile() 来装载dex文件

  6. 调用DexPathlist的findClass(),其实就是遍历Elements集合,对每个Element,DexFile dex = element.dexFile 取出dexFile,并调用loadClassBinaryName()来加载class

  7. 热修复的原理

  8. 热修复步骤:
    a. 定义好要修复的java文件,先编译为class,再编译为dex
    b. 也可以创建个压缩包或者apk文件或者jar包,但要求是apk文件解压后里面必须有一个class.dex 的文件,因为DexPathList类中的loadDexFile方法中会对这个做判断,如果没有就会报异常。
    c.

1
2
3
4
5
6
7
8
9
10
11
12
13
1. PathClassLoader pathLoader = (PathClassLoader) appContext.getClassLoader();
2. DexClassLoader dexLoader = new DexClassLoader(
dex.getAbsolutePath(),// 修复好的dex(补丁)所在目录
fopt.getAbsolutePath(),// 存放dex的解压目录(用于jar、zip、apk格式的补丁)
null,// 加载dex时需要的库
pathLoader// 父类加载器
3.合并 Object dexPathList = getPathList(dexLoader);
Object pathPathList = getPathList(pathLoader);
Object leftDexElements = getDexElements(dexPathList);
Object rightDexElements = getDexElements(pathPathList);
4. 合并完成 Object dexElements = combineArray(leftDexElements, rightDexElements);
5. 重写给PathList里面的Element[] dexElements;赋值 Object pathList = getPathList(pathLoader);// 一定要重新获取,不要用pathPathList,会报错
6. setField(pathList, pathList.getClass(), "dexElements", dexElements);

9 . 各热更新框架的差异

阿里andFix hook 方法在native的具体字段。art虚拟机上是一个叫ArtMethod的结构体。通过修改该结构体上有bug的字段来达到修复bug方法的目的,但这个artMethod是根据安卓原生的结构写死的,国内很多第三方厂家会改写ArtMethod结构,导致替换失效。

qq的dex插装就类似上面分析的那种。通过将修复的dex文件插入到app的dexFileList的前面,达到更新bug的效果,但是不能及时生效,需要重启。但虚拟机在安装期间会为类打上CLASS_ISPREVERIFIED标志,是为了提高性能的,我们强制防止类被打上标志是否会有些影响性能

美团robust 是在编译器为每个方法插入了一段逻辑代码,并为每个类创建了一个ChangeQuickRedirect静态成员变量,当它不为空会转入新的代码逻辑达到修复bug的目的。有点是兼容性高,但是会增加应用体积

网络请求框架:

volley
适合轻量 频率快的网络请求
因为volley的异步任务 其实用的是一个默认数量为4的固定数量线城池,超过4个线程会排在等待队列中阻塞。
volley为了提高速度 做了一些优化:

  1. 请求是用队列维护的 而且可以设置队列的出入方式
  2. 对response做了缓存,如果下次请求的内容没变会优先从缓存中获取
  3. 网络请求的数据 是在工作线程中进行解析并分发到主线程,所以volley的回调是在主线程中,可以直接操作ui,okhttp是回调在工作线程的。
  4. 重要】:volley为了提高访问速度,会将整个response加载在内存中,所以如果下载的文件太大 会引发oom,但是它存储在内存中又是一个缓冲池实现的,ByteArrayPool,每次需要保存数据 先看缓冲池中有无可用空间,有的话就可以直接复用,减少了内存分配的次数,所以比较适合小数据量 但是频繁访问的场景。

换肤

  1. 主要是反射获取AssetManager 然后反射调用
    AssetManager的addAssetPath() 方法
  2. 将自定义的资源文件(可以是zip 或者apk的路径)加入AssetManager之后,重新构造一个Resource对象。

    1
    new Resources(assetManager,context.getResources().getDisplayMetrics(),context.getResources().getConfiguration());
  3. layoutInflator.Factory()对象是一个会根据布局树来依次生成对应view的类,我们可以hook这个类的方法,对其生成相关view的逻辑做修改,比如就可以通过下面的方式,将id = R.id.text的一个TextView 改为一个button

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    LayoutInflater.from(this).setFactory(new LayoutInflater.Factory() {

    @Override
    public View onCreateView(String name, Context context, AttributeSet attrs) {
    if ("TextView".equals(name)) {
    Log.e(TAG, "name = " + name);
    int n = attrs.getAttributeCount();
    //打印所有属性标签
    for (int i = 0; i < n; i++) {
    Log.e(TAG, attrs.getAttributeName(i) + " , " + attrs.getAttributeValue(i));
    }
    for (int i = 0; i < n; i++) {
    if (attrs.getAttributeName(i).equals("id")) {
    String attributeValue = attrs.getAttributeValue(i);
    String id = attributeValue.substring(1, attributeValue.length());
    if (R.id.text == Integer.valueOf(id)) {
    Button button = new Button(context, attrs);
    button.setBackgroundColor(Color.RED);
    return button;
    }
    }
    }
    }
    return null;
    }
    });
    setContentView(R.layout.activity_main);
  1. 为所有activity写好基类,在基类的onCreate()方法中,将我们自定义的LayoutInflator.Factory设置给当前activity的LayoutInflator。
  2. 在自定义的LayoutInflator中,首先根据我们自己定义的tag来判断是否需要换肤,如果需要,先通过layoutInflate将当前view创建出来,然后将需要换肤的view的属性值去除一些带“@”符号的资源引用的,做成一个集合,集合的每个成员里都带有当前需要换肤的view 以及view需要更换的属性(比如 字体颜色,背景色之类的)
  3. 通过前面创建的皮肤资源Resource 来获取需要婚换肤的资源,比如对应的夜间模式的颜色,drawable之类。
    mResources.getIdentifier(resName, "color", skinPackageName);

    lrucache的实现原理

    Lrucache在初始化的时候 需要指定缓存大小 以及缓存文件大小的计算方法 sizeof()
    内部实现其实是用了一个LinkedHashMapL<k,v> 来实现保存缓存文件的。

    每次添加缓存文件到缓存中时,都会调用trimToSize()方法。先计算当前缓存文件大小,与已使用缓存大小做比较,当缓存文件不够用的时候会调用LinkedHashMap 的eldest查找到最久未使用的文件,将其从hashMap中删除,将缓存文件加入hashMap中同时更新已使用缓存大小。

crash的相关处理:

java crash

  1. 查看崩溃类型 是否是哪几种特定类型比如badTokenException 一般是activity正在销毁但是Fragment要弹吐司或者dialog导致的。一般是异步请求会出现这种情况
  2. 查看是否进行过异常处理,有些方法的执行过程出错了,但是因为在代码层进行了trycatch,导致应用并没有崩溃,但是传递了错误的执行结果导致后面的方法出现crash
  3. 查看崩溃机型,有些rom厂商会对rom进行深度定制导致部分方法会crash。尤其国产厂商会对权限做更严格的限制,稍有不慎就会导致意料之外的结果,比如华为手机7.0之后申请牌照权限 除了要申请camera之外,还要申请读写外部存储。
  4. 注意异步操作,可能会内存泄漏导致溢出。
  5. 自定义一个UncaughtExceptionHandler 因为当appcrash的时候,系统会恢复activitiy栈的的第一个activity,有时候会导致不停崩溃,最好在handler中将应用任务栈中的activity清空,并调用System.exit(0)
  6. 还是使用uncaughtExceptionHandler来捕获crash堆栈信息并上传服务器。
  7. 通过替换ActivityThread的H handler对象来达到统一处理异常,并处理异常Activity的声明周期,减少不必要crash的发生。通过
  8. 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    Looper.getMainLooper.post(new Runnable(){
    void run(){
    while(true){
    try{
    Looper.loop();
    }
    catch{
    handleException();
    }
    }
    }
    })

    native crash

  9. 检查对应abi 版本 使用对应的工具

  10. 查看错误地址,判断是不是空指针异常
  11. 用add2line还原堆栈,看调用方法信息
  12. 如果还分析不出为什么出错,就要还原当时的寄存器现场和内存现场

    JNI技术相关

  13. jni的方法可以动态注册 也就可以静态注册 在java层写好native 方法名,如果是 动态注册 将方法的全路径名写上 将点 换成下划线 比如 `JNIEXPORT void JNICALLJava_android_media_MediaScanner_processFile

    (JNIEnv *, jobject, jstring,jstring, jobject);
    

    , 动态注册 通过JniNativeMethod结构体列表来实现,static JNINativeMethod gMethods构建一个JniNativeMethod结构体 需要传入方法名,方法签名(根据传入参数和返回值类型来生成的一定格式的字符串)以及一个jni层对应的函数指针 然后调用AndroidRuntime::registerNativeMethods`来动态注册jni方法

  14. 动态注册 是当调用 system.loadLibrary()时,系统会在native层寻找JNI_OnLoad函数 在这里要完成动态注册的工作

  15. JNIEnv是线程相关的,每个线程独一份儿,当我们要手动从一个native方法回调java层方法时,需要手动传入一个JNIEnv,这个JNIEnv需要从JavaVM构造,JavaVM会在 JNI_OnLoad方法中由jvm传递给native,JNIEnv.AttachCurrentThread 来获取一个当前线程的jniEnv。当然退出的时候还要DetachCurrentThread 来释放资源
  16. 通过JniEnv调用方法 NativeType Call<type>Method(JNIEnv *env,jobject obj,jmethodID methodID, ...)。
  17. 操作成员变量
  18. 1
    2
    3
     NativeType Get<type>Field(JNIEnv *env,jobject obj,jfieldID fieldID)
    //或者调用Set<type>Field系列函数来设置jobject对应成员变量的值。
    void Set<type>Field(JNIEnv *env,jobject obj,jfieldID fieldID,NativeType value)

并发相关

  • 启动一个新线程 继承thread 对象复写run方法 或者创建一个runnable对象,其实这两种方式没有太大区别,本质上Thread 对象就是一个runnable接口的实现类,网上很多人说 头一种方式不面向对象,不能复用资源是由继承 这种创建方式导致的,其实你完全可以new 一个thrad对象 把它像runable一样交给一个thread 同样可以达到复用资源的目的(本质上会吧这个thread 赋值给Thread中的target)
  • 新建线程 使用start() 如果调用run() 实际是在调用线程去执行Thread 中run方法的内容
  • 线程挂起有两种方式:sleep() 不释放锁,wait() 会释放锁
  • 终止线程不要使用stop() 因为该方法过于暴力,会直接终止当前线程并释放锁,可能造成数据不一致 导致安全隐患。
  • 如何实现多线程通讯:通过共享对象,或者使用wait notify,但是要注意假唤醒和唤醒型号丢失,唤醒信号丢失是指 当a线程调用notify时,等待池中尚无等待锁对象的线程,此时信号就丢失了,当b线程再调用wait的时候就永远不会被唤醒,解决办法是b线程调用wait的时候,先判断一下有无线程调用过notify。 假唤醒是指有时候线程会因为一些莫名其妙的原因在没有notify的时候被jvm唤醒,解决办法是使用while循环轮训判断是否有线程调用过nofity,这种类似于java的自旋,但是比较消耗cpu。记住不要对字符串常亮或者全局对象中使用wait和notify。
  • 如何停止一个正在运行的线程? 设置一个判断标识。通常是一个volentaile类型的变量。如果标识被设置为false 就停止运行,不要直接调用stop。
  • suspend 和 resume 方法已经过时了,因为这两个方法使用时序不当会非常容易造成死锁。 因为suspend 会让线程挂起吗,但是却不会释放锁。只有当该线程被其他线程调用了resume() 之后 才会继续执行。但如果其他线程调用t1的resume时要获得锁,这时候死锁就产生了 t2始终无法获得锁,t1永远无法被唤醒。
  • reentranLock 可重入锁和syncornoized的比较:
    首先 reentranLock 的设计并不是为了替代syncornized。只是为了在syncornized不能满足使用需求时,为加锁增加一些新的特性。
    syncornized 的缺点:无法中断一个正在等待锁的线程,当多个线程争夺某个锁时,未获得的对象只能不断等待锁对象的释放而不能终端这个过程。对于大量线程争夺锁的情况性能比较低。
    reentranLock 是对固有锁的补充,提供了可中断lockInterruptibly(),可等待tryLock,超时中断 tryLock(long, TimeUnit),公平和非公平锁的实现。而且在某些jvm版本上提供了比固有锁更好的性能。 但是不会自动释放锁,需要在final块中 手动实行释放操作。

    新的jdk已经对固有锁做了很多优化,尤其是针对固有锁无法中断导致多线程争夺场景下性能低的情况。比如增加了偏向锁,轻量级锁和重量级锁的特性。
    当多线程的执行过程类似于顺序执行时,jvm会默认使用偏向锁来提高并发性能,当一旦出现多线程争夺,便会膨胀为重量级锁。

    相同点:都是可重入锁

  • 多线程协作: 主要是解决同步互斥、资源互斥、协调竞争的问题。可以使用syncnornized lock wait notify 以及信号量Semaphore、CountdownLatch来解决互斥同步问题。 注意 sleep会抛出InteruptException异常。

  • 乐观锁:每次操作时不加锁,而是假设没有冲突去完成某项操作,如果因为冲突失败,就去重试知道成功为止。 可以使用volatile 和 cas原语实现(a.compateAndSet(oldValue,newValue));
    在操作前每次先读取这个volatile修饰的字段,判断与旧值是否相等,相等则操作,否则重试。可以避免阻塞。
  • 悲观锁:每次操作前先尝试获取锁,获取不到就等待,直到某个线程释放了锁为止。可以使用syncornize和lock实现。 悲观锁会频繁的导致线程挂起和恢复执行,这个开销非常重量级,会导致时间代价比较大
  • 判断线程是否拥有锁: Thread.holdsLock() 返回true表示拥有一个具体锁
  • 如果你提交任务时,线程池队列已满。会时发会生什么?
    这个问题问得很狡猾,许多程序员会认为该任务会阻塞直到线程池队列有空位。事实上如果一个任务不能被调度执行那么ThreadPoolExecutor’s submit()方法将会抛出一个RejectedExecutionException异常

  • Interupt()方法会将isInterupt置为false。 但是不可以对一个已经调用了sleep() 的方法执行interupt,会抛异常,而且在异常块里,会将isInterupt的值置为true。

  • 为什么进行指令重排序:为了提高效率,现代cpu都是采用流水线来执行指令。一个操作会被分为:取指令,译码,访存,执行,写回等若干阶段,多条指令可以同时存在在流水线中并行执行,为了避免一条指令的执行过程太长导致后续指令都卡在执行之前,cpu会对令重拍以提高效率。
  • Condition 是与lock配合使用的,也称为条件队列 或条件变量,主要作用是 a: 以原子方式 释放相关的锁,并挂起当前线程 b: 唤醒相关等待队列的线程,是为了解决Object.wait/notify/notifyAll 不好用而出来的。 wait 和 notify 只有一个阻塞队列,如果在生产者和消费者模型中,想当产品满了唤醒一个消费者线程 如果使用wait,无法保证唤醒的一定是消费者线程,因为只有一个阻塞队列,队列中可能会有生产者线程 也可能会有消费者线程,wait会从等待队列中随机唤醒一个 所以wait 是有局限性的。 我们可以实现两个Condition 条件队列,生产者线程一个 消费者线程一个 当我们想唤醒对应线程 只需要调用对应的条件队列就可以了。如果是用syncornized 对应的就是wait 和 nofity。 如果是用并发包的lock reentranlock 对用的就是await signal signalAll。

  • Semaphore 等于是个共享锁 和Lock类似 可以为他设置值,如果设为1 就等于lock

  • ThreadLocal的实现: ThreadLocal set的时候,会传入一个Thread对象。通过Thread对象获取其中的成员变量ThreadLocalMap threadLocals ,如果没有就创建,有就往里面写值。 可以看到 ThreadLocal真正存储数据其实是放在Thread对象中的,这就是为什么ThreadLocal可以做到线程隔离的原因,但是ThreadLocalMaps其实是ThreadLocal的内部类,所以Thread对象应该会持有ThreadLocal才对,但是因为ThreadLocalMap 中的key 其实是Threadlocal的弱引用。所以并不会出现内存泄漏的问题。使用ThreadLocal 也可以做到被其他线程访问,比如InheritableThreadLocal对象中的 数据就可以被其他线程访问。 子线程访问父线程的InheritableThreadLocal的值时,使用了浅拷贝。
  • Java中long和double赋值不是原子操作,因为先写32位,再写后32位,分两步操作,这样就线程不安全了。如果改成下面的就线程安全了

并发相关的集合:

  • currentHashMap :通过锁分段技术保证并发环境下的写操作;

    通过 HashEntry的不变性、Volatile变量的内存可见性和加锁重读机制保证高效、安全的读操作;

    通过不加锁和加锁两种方案控制跨段操作的的安全性。

    HashMap 当在并发情况下进行扩容重哈希的时候,可能在链表中形成闭环,这样在进行查找 插入 删除操作的时候会陷入死循环。CorrentHashMap 解决了这个问题。

    分段锁的实现依赖于Segment ,继承ReentranLock,里面有一个计数器变量mCount表示自己管理的table中hashEntry数量,每次插入删除元素都会更新这个值。成员变量table,表示自己管理的HashEntry链表。当写操作发生在不同的segment段时,可以允许多个写线程并发执行。
    HashEnrty是个四元组,key,hash和next域都被声明为final的,value域被volatile所修饰,因此HashEntry对象几乎是不可变的,这是ConcurrentHashmap读操作并不需要加锁的一个重要原因。
    这意味着,我们不能把节点添加到链表的中间和尾部,也不能在链表的中间和尾部删除节点。这个特性可以保证:在访问某个节点时,这个节点之后的链接不会被改变,这个特性可以大大降低处理链表时的复杂性
    注意 :CorrentHashMap 不允许插入null的key 和value 而HashMap是可以的。
    CorrentHashMap 初始化的时候 默认大小是2的n此方。
    jdk 1.8以后,已经将segment的同步机制更改为了Syncornized和CAS操作。 说明Syncornized的性能已经优化到不比ReentranLock差了。

  • JDK对Syncnonized的优化 : 引入偏向锁,轻量级锁和重量级锁。
    synchronized的执行过程:

  1. 检测Mark Word里面是不是当前线程的ID,如果是,表示当前线程处于偏向锁
  2. 如果不是,则使用CAS将当前线程的ID替换Mard Word,如果成功则表示当前线程获得偏向锁,置偏向标志位1
  3. 如果失败,则说明发生竞争,撤销偏向锁,进而升级为轻量级锁。
  4. 当前线程使用CAS将对象头的Mark Word替换为锁记录指针,如果成功,当前线程获得锁
  5. 如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。
  6. 如果自旋成功则依然处于轻量级状态。
  7. 如果自旋失败,则升级为重量级锁
  • copyOnArrayList 写时复制机制。
    add元素的时候,先加锁,然后拷贝一份相同的数组,在新数组上操作,写完之后将对原数组的引用修改为对新数组的引用。 读可以不用加锁读。
    1、如果写操作未完成,那么直接读取原数组的数据;
    2、如果写操作完成,但是引用还未指向新数组,那么也是读取原数组数据;
    3、如果写操作完成,并且引用已经指向了新的数组,那么直接从新数组中读取数据
    缺点:写会拷贝数组,如果容量很大容易频繁触发gc
    不能实时读,但保证最终一致性。
    适合都多写少的场景,但要慎用,因为容易oom

  • CopyOnWriteHashSet

  • HashTable 解决hash冲突:链地址法
  • BlockQuene 阻塞队列:一般用来解决生产者消费者问题。提供了四组方法,分别产生四种结果,抛异常,返回特定值,阻塞,超时。无参构造方法默认是误解队列,也可以创建时默认设置一个缓冲区大小。
    linkedBlockedQuene 本质还是一个单向链表,为了提高生产和消费的效率,使用了两个锁分别对表头和表尾数据进行同步,如果take() 方法执行时队列为空,线程会阻塞在await上。只有等put()方法执行之后,会唤醒一个线程取一条数据。

并发包中 锁机制的实现

其实就是通过CAS机制 在lock的时候 CAS 在unlock的时候讲atom值设置回去,如果CAS失败 就enquene 将当前线程记录在链表中,然后挂起线程 LockSupport.park(), LockSupport.park()会调Unsafe.park()的native方法,虚拟机在linux中执行pthread_mutex_lock函数 实现阻塞线程的操作。用当其他占有锁的线程 释放锁的时候,会调用Usafe.unpark唤醒阻塞队列的对头线程,线程继续递归调用lock()方法尝试获取锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void lock() {
// step 1. cas 尝试 检查当前是否有其他线程改写了值
boolean ok = state.compareAndSet(0, 1);

// step 2. 如果没有 线程将值改写为1 同时独占该方法
if (ok) {
setExclusiveThread(Thread.current()); // 这只是个标志位,不用太介意
return;
}

// step 3. 如果cas失败 就进不去步骤2 当前线程要被阻塞 此时执行入队 用队列记录被阻塞的线程
enqueue();

// step 4. park方法将当前线程挂起 停止调度 线程就执行到这里阻塞 当其他独占线程释放锁的时候 会调用unsafa.Unpark() 会将阻塞队列对头线程唤醒 此时线程可以往下执行到step5 尝试递归获取锁
Unsafe.park();

// step 5. retry
lock();
}

这些步骤可以提取出共有的特性 dogue lee 就写了一个框架 叫AbstractQueuedSynchronizer 简称AQS框架 是通用的锁框架
AQS 伪代码

1
2
3
4
5
6

public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}

步骤分为四步:

  • tryAcquire,抽象方法,由子类实现,子类通过控制原子变量来表示是否获取锁成功,类似于上文代码的
    Step1、Step2
  • addWaiter,已经实现的方法,表示将当前线程加入等待队列,类似于上文的Step3acquireQueued(),*
  • 挂起线程,唤醒后重试,类似于上文的Step4、Step5
  • 处理线程中断标志位。

如果需要自定义一个锁 只需要复写tryAqcuire方法 根据具体逻辑来由子类控制原子变量是否成功获取锁

可重入与不可重入锁 其实就是tryAcquire这地方的逻辑不一样,不可重入锁 一旦cas失败直接就返回了
可重入锁内部会有一个持有锁的线程信息,并在cas失败的时候判断,如果线程信息是一致的 将原子变量+1就好。当然 解锁的时候还要对应-1 重入了几次 就要解锁几次,不然原子变量的值无法恢复为原始值。

并发的底层实现:

volatile

  1. 禁止指令重拍(保证访问次序)
  2. 对线程强制可见(存强制刷新回主存,读强制从主存读取)

class Singleton{
private volatile static Singleton instance = null;

private Singleton() {

}

public static Singleton getInstance() {
    if(instance == null) {
        synchronized (Singleton.class) {
            if(instance == null)
                instance = new Singleton();
        }
    }
    return instance;
}

}

这里为什么要使用volatile修饰instance?主要在于instance = new Singleton()这句,这并非是一个原子操作,事实上在JVM中这句话大概做了下面3件事情:

(1)给instance分配内存

(2)调用Singleton的构造函数来初始化成员变量

(3)将instance对象指向分配的内存空间(执行完这步instance就为非null了)。

但是在JVM的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在3执行完毕、2未执行之前,被线程二抢占了,这时instance已经是非null了(但却没有初始化),所以线程二会直接返回instance,然后使用,然后顺理成章地报错。

1.可见性的保证:

算机在运行程序时,每条指令都是在CPU中执行的,在执行过程中势必会涉及到数据的读写。我们知道程序运行的数据是存储在主存中,这时就会有一个问题,读写主存中的数据没有CPU中执行指令的速度快,如果任何的交互都需要与主存打交道则会大大影响效率,所以就有了CPU高速缓存。CPU高速缓存为某个CPU独有,只与在该CPU运行的线程有关。

有了CPU高速缓存虽然解决了效率问题,但是它会带来一个新的问题:数据一致性。在程序运行中,会将运行所需要的数据复制一份到CPU高速缓存中,在进行运算时CPU不再也主存打交道,而是直接从高速缓存中读写数据,只有当运行结束后才会将数据刷新到主存中。

volatile 使用了缓存一致性协议:
缓存一致性协议(MESI协议)它确保每个缓存中使用的共享变量的副本是一致的。其核心思想如下:当某个CPU在写数据时,如果发现操作的变量是共享变量,则会通知其他CPU告知该变量的缓存行是无效的,就是在cpu缓存中写入一个缓存状态标记位:Invalid,因此其他CPU在读取该变量时,先读取标记位。发现其无效会重新从主存中加载数据(汇编代码会在指令前+LOCK字段。)

2 访问一致性:(禁止指令重拍)

lock 指令会保证 在lock指令前的指令 在lock前执行 在lock指令后的 在lock执行完执行。
lock汇编指令就是所谓的内存屏障。

syncornized

实现原理:
对当前对象和引用对象加锁:
syncornized(this)
syncornized(object.class)
system.out.println()
都是sync方法

反编译classa字节码 会发现在方法执行前有两个指令

monitorenter
monitorExist
如果修饰的是方法 在常量池中 方法对应的访问标记位 会添加一个ACC_SYNCHRONIZED访问标记
在执行该方法时,原理还是一样 操作monitor监视器对象来实现的。

Monitor 是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个monitor关联(对象头的MarkWord中的LockWord指向monitor的起始地址),同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。其结构如下
-w173
Owner:初始时为NULL表示当前没有任何线程拥有该monitor record,当线程成功拥有该锁后保存线程唯一标识,当锁被释放时又设置为NULL;
EntryQ:关联一个系统互斥锁(semaphore),阻塞所有试图锁住monitor record失败的线程。
RcThis:表示blocked或waiting在该monitor record上的所有线程的个数。
Nest:用来实现重入锁的计数。
HashCode:保存从对象头拷贝过来的HashCode值(可能还包含GC age)。
Candidate:用来避免不必要的阻塞或等待线程唤醒,因为每一次只有一个线程能够成功拥有锁,如果每次前一个释放锁的线程唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换(从阻塞到就绪然后因为竞争锁失败又被阻塞)从而导致性能严重下降。Candidate只有两种可能的值0表示没有需要唤醒的线程1表示要唤醒一个继任线程来竞争锁。

Unsafe对象

是java与native交互的中间者
Unsafe.park() 阻塞线程
Unsafe.unPark() 回复线程
Unsafe.compareAndSwapInt() CAS操作

CAS原理:

原理

CAS(比较与交换,Compare and swap) 是一种有名的无锁算法。CAS, CPU指令,在大多数处理器架构,包括IA32、Space中采用的都是CAS指令,CAS的语义是“我认为V的值应该为A,如果是,那么将V的值更新为B,否则不修改并告诉V的值实际为多少”,CAS是项 乐观锁 技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。


1
2
3
public final boolean compareAndSet(int expect, int update) {   
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

设置之前先比对 值是否与预期一致,一致就设置为预期值,不一致就重试直到一致为止。(注意 是一直重试,不是阻塞式,当cas失败 线程并不会被阻塞。)

因为cas的对象是被volatile关键子修饰的,它保证可见性和禁止指令重拍

CAS缺点

CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题。ABA问题,循环时间长开销大和只能保证一个共享变量的原子操作

  1. ABA问题。因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。

从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

关于ABA问题参考文档: http://blog.hesey.net/2011/09/resolve-aba-by-atomicstampedreference.html

  1. 循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
  1. 只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。

SAQ模型

并发的实战问题

  1. 如何让一段程序并发执行,并最终汇总结果
    目前想到两种实现方式:

    1. 采用fork/join实现方式
      定义一个RecurisiveTask 这种Task继承Future,可以返回一个结果。
      在该Task中定义一个阈值,复写compute()方法,在该方法里不超过阈值就不fork新线程,直接计算并返回 ,超过就将任务分割 再创建两个task 并调用fork方法。 调用join等待两个线程执行完将结果合并。伪代码如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    public static void main(String[] args) {
    CountTask t = new CountTask(1,7);
    ForkJoinPool pool = new ForkJoinPool();
    Future<Integer> future = pool.submit(t);
    }

    static class CountTask extends java.util.concurrent.RecursiveTask<Integer>{
    private int threadhold = 2;
    int start;
    int end;
    CountTask(int start,int end){
    this.start = start;
    this.end = end;
    }
    @Override
    protected Integer compute() {
    if((end -start)>threadhold){
    return start +end;
    }
    else {
    int mid = (int) ((end-start)/2+start);
    CountTask t1 = new CountTask(start,mid);
    CountTask t2 = new CountTask(mid,end);
    t1.fork();
    t2.fork();
    int result1 = t1.join();
    int result2 = t2.join();
    return result1+result2;
    }
    }
    }

2.使用线程池 创建多个futureTask 并阻塞等待get()方法返回值。

  1. 如何合理的配置java线程池?如CPU密集型的任务,基本线程池应该配置多大?(少配置一些核心线程数,一般和cpu核心数一致, 为了使所有线程都能使用到cpu。)IO密集型的任务,基本线程池应该配置多大?(大部分线程都阻塞,所以要多配置核心线程数,一般使用cpu核心的两倍)用有界队列好还是无界队列好?(没有好坏之分,要分业务场景,一般配置有界队列。当有可能会出现爆发式增长的场景,使用无线队列,会不断增长直到内存耗尽。)任务非常多的时候,使用什么阻塞队列能获取最好的吞吐量?(使用非阻塞队列并结合cas操作以获得更好的吞吐量)
  2. 什么场景使用可重入锁:syncornized无法满足需要的情况,包括可中断,可超时,公平,非公平等高级特性时使用ReentranLock
  3. 什么场景下可以使用volatile替换synchronized?只需要可见性,不需要保证原子性时可以代替。volatile不适用那种新值依赖旧值的场景
  4. CountDownLaunch 一般这么用:主任务等待n个子任务完成,创建一个CountDownLaunch(n),子任务执行一次,CountDownLaunch.countDown()一次。主任务调用CountDownLaunch.await()阻塞等待, 只有CountDownLaunch倒数完成,主任务才会继续执行。
  5. 使用阻塞队列实现生产者消费者模型:
    伪代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public static void main(){
LinkedBlockedQueue queue = new LinkedBlockedQueue(10);
class producer extends Runnable{
Producer(BlockedQueue queue){
this.queue = queue;
}
void run(){
try{
while(true)
queue.put()
}
catch (InteruptException e){}

}
}

clase Consumer extends Runnable{
BlockedQueue queue;
Consumer(BlockedQueue queue){
this.queue = queue;
}
void run(){
try{
queue.take();
}catch(Exception e){...}
}
}

new Thread(new Producer(queue)).start();
new Thread(new Consumer(queue)).start();

}

MVVM 架构的演进

参考链接

Rxjava相关

  1. 什么时候将订阅者和被订阅者关联起来的。
    在subscribe()方法调用的时候。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 等于是日程表,告诉订阅者要依次做什么
class OnSubscribe(Subscriber subscriber){
void call(subscriber){
subscriber.onNext();
subscriber.onNext();
subscriber.onComplete();
}
}

class Observable{
...
public Subscription subscribe(Subscriber subscriber){
subscriber.onStart();//先执行onStart() 做些准备工作 注意这个方法无法切换线程只能在事件发送端的线程执行。
if(!subscriber instance of SafeSubscriber){
subscriber = new SafeSubscriber(subscriber);// 将传入的subscriber包装为safeSubscriber 为了保证onComplete onError 只被执行一次。
}
onSubscribe.call(subscriber);// 手动调了上面日程表的call方法。开始发送时间序列
return subscriber;
}
}
  1. flatMap observerOn() subscribeOn() 切换线程的原理:lift()变换

可以这么理解:

每次lift() 都会创建一个新的Observable,因为链式调用的缘故,最终subscribe()将观察者和被观察着绑定关系的时候,都会绑定到最后一次lift()变换生成的新的Observable上,当调用subscribe()方法的时候,会先调用最终那个subscriber的onStart(),然后调用新Observable() 的OnObservable()方法,在新Observable()中,它的onObservable被lift操作很机智的修改了,它首先创建了一个新的subsciber 这个subscriber持有原来那个订阅着的引用,然后它会调用上一级的那个obServable ,也就是调用lift()方法那个被观察者的call()方法,让自己创建的新的观察者来观察原来那被观察者发送的序列。 在这个心的被观察者中 它可以将原来观察者发送的消息处理一下,然后再发给最终的观察者。

说的非常绕口,用一个形象的例子来说明一下:

建立简单的观察者被观察者并建立关联关系。

假设一个场景是部队操练表演。 我们是军官A(最原始的那个Observable)表演动作的流程由我们制定,基地里所有的士兵(Subscriber)都可以主动申请参加表演(订阅)。我们制定了一个动作流程表(OnObservable对象),表里我们制定好了 “先左转(onNext),然后右转(onNext),然后稍息(onNext),然后立正,最后敬礼,结束(onComplete)!”这套表演流程。这个表上有个签名栏,谁说要表演,就要在签名栏签上自己的大名。(OnSubscribe的 call方法 会传入一个观察者)。当然我们基地的士兵还是新人,只能执行一些比较简单的动作,复杂的动作不会做。

现在有一个士兵主动请缨要表演了(Observable.subscribe(new Subscriber()),军官会告诉他你可以先准备一下,(subscribe 方法会在里面先调用一下 Suscriber的onStart(),观察者可以先做些准备工作),然后士兵在流程表上签上了自己的名字(调用Onsubscribe.call(subscriber),将观察者自己传入call()方法中)。

要开始表演了,军官开始依次按照流程表的动作开始喊口号“士兵A,左转!”(被观察着会等subscribe方法调用之后才开始发送事件序列。)士兵A完成了一次左转动作。军官一个口号一个口号的喊完了,士兵也完整执行了所有的动作,并敬礼(onComplete()调用了)。

使用Lift变换

上面说过 士兵只可以执行简单的操作,复杂的操作还不会,如果我们要他做一个托马斯回旋,他不明白什么意思,但是你要是跟他说,就是先转一圈,再转一圈,他就明白了。

这时候可以这么做: 我们还是按照名单上的流程念,但是这时候名单变为了”先转体360度,再立正敬礼结束!”,士兵A不懂转体360是什么意思,这时候军官B 出来了,他有个手下士兵B 跟表演的士兵关系很好(士兵B 是 新构建的Subscriber,他存在在lift变换新创建出来的观察者内)。士兵b比较有文化,知道360转体就是“转一圈再转一圈的意思”。为了帮助士兵A顺利完成任务,他决定,将军官A的名单上名字改成自己的,现在士兵B现在直接面向军官A。他收到军官A的消息后 处理之后再告诉士兵A怎么做。

现在准备表演了。士兵A对军官B说 “我准备好了”,(Observable.subscribe()),军官B 收到了(在lift方法创建的新的Observable的subscibe()方法中,先创建了一个新的Subscriber(old subscriber),然后调用上一个Observable中的OnSubscribe()方法,开始发送原始观察者中的事件,不过这些时间都被新的观察者接收了),通知军官A开始喊口号(调用旧的OnSubscribe对象中的call方法,此时call传入的是新的观察者)。

军官A 叫到“士兵B , 转体360”,士兵B收到,翻译了一下“士兵A,转体360 就是 转一圈,再转一圈”的意思。
士兵A 按照B 说的 ,顺利完成了这次表演。

rxjava 的线程切换

subscribeOn() 和 observeOn() 都做了线程切换的工作(图中的 “schedule…” 部位)。不同的是, subscribeOn() 的线程切换发生在 OnSubscribe 中,即在它通知上一级 OnSubscribe 时,这时事件还没有开始发送,因此 subscribeOn() 的线程控制可以从事件发出的开端就造成影响;而 observeOn() 的线程切换则发生在它内建的 Subscriber 中,即发生在它即将给下一级 Subscriber 发送事件时,因此 observeOn() 控制的是它后面的线程

图中共有 5 处含有对事件的操作。由图中可以看出,①和②两处受第一个 subscribeOn() 影响,运行在红色线程;③和④处受第一个 observeOn() 的影响,运行在绿色线程;⑤处受第二个 onserveOn() 影响,运行在紫色线程;而第二个 subscribeOn() ,由于在通知过程中线程就被第一个 subscribeOn() 截断,因此对整个流程并没有任何影响。这里也就回答了前面的问题:当使用了多个 subscribeOn() 的时候,只有第一个 subscribeOn() 起作用。

关键成员

Subscriber:

继承Observable 比它多了几个方法
onStart()
unSubscribe() 用来接触绑定 因为在 subscribe() 之后, Observable 会持有 Subscriber 的引用,这个引用如果不能及时被释放,将有内存泄露的风险

里面有个成员Producer 用来处理跟Observable的背压问题
toRequest 为请求数 Producer用的 用来请求数据源发送的数据数
可以调用setProducer() 为subscriber 设置Producer
调用request() 为其设置请求数

首先,如果在设置请求数时还没有初始化Producer,就进行累加保存。直到Producer被设置时,如果当前Subscriber未设置请求数且内部Subscriber不为空就把Producer赋值给内部的Subscriber,否则就会赋值给当前的Subscriber。要是当前Subscriber至今未设置请求数,就请求Long.MAX_VALUE数量的数据,多余部分就会忽略。

Scheduler 线程调度

Scheduler 线程调度器 其中真正实现线程切换的是Worker

Worker实现了Subscription接口 所以Worker具有取消订阅的功能。

  • 当调用SubscriberOn(Scheduler.newThread())时,创建了一个新的OperatorSubscribeOn对象,并将我们的Scheduler作为参数传递了进。 OperatorSubscribeOn本身也是一个Observable,在它的OnSubScribe()的call方法中,其实可以猜到,它一定会通过某种手段,将我们原始的Observable的发送线程切换到目标线程。 那RxJava是怎么做的呢?
  1. 重写 OperatorSubscribeOn的onSubscribe()的call()方法
  2. 根据传入的Scheduler 对象 创建了一个worker对象
  3. 执行worker的schedule()方法 传入了一个Action()
  4. worker 的schedule()方法会保证在目标线程中执行Action0的call()方法
  5. 在Action0的call()方法中 创建了一个新的Subscriber
  6. 这个新的Subscriber是原观察者和被观察者之间的中间人
  7. 我们在worker线程中 将原被观察者与新的Subscriber订阅,此时原观察者会在Worker线程中开始发射数据
  8. 数据被我们的新Subscriber转发给原Suscriber
  9. 这样数据就转到我们自定义的线程中发送了 就完成SubscribeOn的目的
map()函数:

创建了一个新的Observable 返回
new OnSubscribeMap<T, R>(this, func)
当被订阅时,会调用新Observable的call方法

1
2
3
4
5
6
7
8
9
10
@Override
public void call(final Subscriber<? super R> o) {

// 对传入的 Subscriber 进行再次封装成 MapSubscriber
// 具体 Observable.map() 的逻辑是在 MapSubscriber 中
MapSubscriber<T, R> parent = new MapSubscriber<T, R>(o, transformer);
o.add(parent); // 加入到 SubscriptionList 中,为之后取消订阅

// Observable.interval() 返回的 Observable 进行订阅(关键点)
source.unsafeSubscribe(parent);

创建了一个新的Subsriber对象。他会对原来发送的消息进行处理

看一下它的onNext方法

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public void onNext(T t) {
R result;

...

result = mapper.call(t); // 数据进行了变换

...

actual.onNext(result); // 往下流传
}
interval()函数

构建流程几乎类似 唯一的区别是新创建的Subscriber对象的call方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public void call(final Subscriber<? super Long> child) {
final Worker worker = scheduler.createWorker();
child.add(worker);
worker.schedulePeriodically(new Action0() {
long counter;
@Override
public void call() {
...

child.onNext(counter++);

...
}

}, initialDelay, period, unit);
}
rxjava 如何保证串行发射的

发送者循环 和队列漏

高级方法: backPressure 背压

所谓背压 其实就是在异步任务过程中,发送者的事件流太快,接收方来不及处理,告诉上游发送者降低时间发送速率的一种策略。
其实就是被观察者支不支持观察者通过调用request(int length) 来手动通知被观察者发送length数量的数据。

这个需要rxjava的被观察者支持才行,1.x版本的有部分是不支持的比如interval,timer等操作符创建的Observable。而类似range创建的Observable 是支持request这样的背压请求的。

不支持背压的Observable 该如何做到流程控制呢:

  1. 抛弃部分数据
1
2
3
这个操作符简单理解就是每隔200ms发送里时间点最近那个事件,
//其他的事件浪费掉
.sample(200,TimeUnit.MILLISECONDS)
  1. 缓存数据 处理不过来可以先缓存一部分 buffer(包装为list) window(包装为新Observable)操作符
1
2
//这个操作符简单理解就是把100毫秒内的事件打包成list发送
.buffer(100,TimeUnit.MILLISECONDS)
  1. 使用特殊的两个操作符,使其可以相应request请求
  • onBackpressurebuffer(int buffersize):把observable发送出来的事件做缓存,当request方法被调用的时候,给下层流发送一个item(如果给这个缓存区设置了大小,那么超过了这个大小就会抛出异常)。
  • onBackpressureDrop:将observable发送的事件抛弃掉,直到subscriber再次调用request(n)方法的时候,就发送给它这之后的n个事件。

小细节: range 默认缓冲是16个事件。zip 128个事件缓冲区

rxjava2

将支持背压和不支持背压分开
不支持背压的:Observable/Observer
支持背压:Flowable/Subscriber
Flowable 可以通过range这样的操作符创建,也可以通过create方法创建,但是create需要手动制定背压规则 BackpressureStrategy.BUFFER

注意: 在观察者调用了request之后,会立刻回调到onNext,而不一定等onSubsbcribe方法走完
其实就是流程控制 为了解决发送端速率与接收端速率不一致的问题

flatMap()

在调用flatMap() 会传入一个Funx(){
},它会根据原始数据源创建一个新的Observable。 猜也能猜出来实现的原理是什么,还是用的lift()变换。比较适合用在需要嵌套请求的情形。比如 先获取token,然后携带token去请求数据,再返回一个新的Observable。

线程池:

当线程数小于核心线程数时,创建线程。
当线程数大于等于核心线程数,且任务队列未满时,将任务放入任务队列。
当线程数大于等于核心线程数,且任务队列已满
若线程数小于最大线程数,创建线程
若线程数等于最大线程数,抛出异常,拒绝任务

Glide

1. Glide的构建流程

  1. 通过AndroidManifest和@GlideModule注解获取用户自定义配置GlideModule,并调用其对应的方法
    通过GlideBuilder构建Glide:
  2. 新建线程池
  3. 新建图片缓存池和缓存池
  4. 新建内存缓存管理器
  5. 新建默认本地缓存管理器
  6. 新建请求引擎Engine
  7. 新建RequestManger检索器
  8. 新建Glide
  9. Glide构造方法中,新建模型转换器,解码器,转码器,编码器,以及生成Glide上下文GlideContext
  10. 通过RequestManager检索器,建立生命周期监听,并建立一个RequestManager
    完成!

转换器用来转换资源id’ File Uri Http链接等 为对应的Stream 后者Fd
编码器用来把流 或者bitmap drawable 编码为文件缓存起来
解码器是解码流 或者 文件 或者ByteBuffer 为对应的drawable 或者bitmap
转码器是吧drawable 或者 bitmap 转码为对应的byteArray()

Glide是如何完成生命周期感知的

Glide.with(context) 会获取一个RequestManager对象, 在获取这个请求管理器之前,
Glide会先判断context是否是后台线程,是的话,获取他的ApplicationContext对象。
用这个context对象先去判断页面是不是销毁了,如果销毁了会直接丢弃这个请求 如果没有销毁,会创建一个感知当前ActivityFragment添加到Activity中,并实现了生命周期LifeCycleCallBack.

通过这个CallBack,Glide做到了生命周期感知。
每个页面会对应一个RequestManager,RequestManager对这些callback做了监听,可以统一管理一个页面的所有请求。跟随Activity生命周期。

Glide.with(context)完成之后就得到了一个RequestManager

Glide的双缓存

Glide不同于Picassa之类的,他们的图片缓存流程完全不一样
Glide会先下载原始图片,然后根据加载view尺寸不同,缓存多个不同大小的图片,再加载图片进view。
Picasso也是先下载原始图片,然后只缓存这一张,下次加载view的时候,会resize后再加载进去。
造成了一下几个区别:

* Glide比Picasso更占缓存
* Glide加载比Picasso更省内存。减少oom
* Glide因为缓存了多张小图,下次遇到同样尺寸可以直接从缓存拿,减少一次resize。会更快

缓存流程

  1. 生成一个uniqueKey 保证唯一 哪怕view宽高改变了,key也会变
  2. LruResourceCache内存缓存,Lru算法,保存缓存图片。
  3. 取出图片时,如果在内存缓存中,直接拿出来用,同时把图片从缓存移除,用一个弱引用HashMap保存当前正在用的图片
  4. 一个acquire来记录图片被引用次数。
  5. acquire为0时,将图片从弱引用map拿出,放进lru里
  6. 如果内存缓存没有 就用DiskLruCache缓存到磁盘

为什么双缓存里 在内存缓存之前 还要有一个HashMap

glide 访问lrucache,用的是remove操作,将remove掉的图片加入activeResource,这,个操作我觉得有两个好处。1:分担lrucache的压力。减少trimToSize的概率。如果正在remove的是张大图,lrucache正好处在临界点,此时remove操作,将延缓lrucache的trimToSize操作。2 提高效率:activeResource用的是hashmap,lrucache用的是linkedhashmap,从访问效率而言,肯定是hashmap高不少。hashmap起一个辅助作用。

Glide gif图优化

可以用giflib so库实现加载git ,由glide负责下载和缓存策略。

缓存策略:

最后Engine的缓存更新策略里,我再补充一点。
Glide的缓存实例,在内存中都是以EngineResoure对象存在的。
除了有一个LruResourceCache作为内存缓存对象外,还有一个叫做ActiveResources,里面维持了一个HashMap<key,WeakRef>用来保存当前仍有引用的EngineResource对象。
缓存的添加过程: 每当构建了一个SingleRequest(调用了一次Glide加载某个资源或者url)开始调用load()时,最终都会交给Engine去执行load任务。Engine先从ActiveResources里拿EngineResource, 如果拿不到再从LruResourceCache里拿,拿到之后会将这个EngineResource从LruResourceCache移除,同时将其添加到ActiveResources中,获取到对应的EngineResource后,会把这个EngineResource对象的引用数acquire + 1。如果内存和活跃引用里都没找到,从历史任务表里找找看有没有过这个key对应的任务,没有就创建并添加,并将load方法传入的callback和job关联起来,根据配置的磁盘缓存策略,交给对应的Executor,如果允许使用磁盘缓存,就是diskCacheExecutor,否则就是sourceExecutor 调度执行解码任务,任务的执行体是DecodeJob。
DecodeJob要拿数据,先看磁盘缓存配置策略,大图小图都缓存,只缓存原始图,或者禁用缓存,都会有不同逻辑去获取对应的数据,不管怎么样,图片数据拿到了,回调onDataFetcherReady(),用这个数据解码成对应的BitmapResource,或者BytesResource或者DrawableResource之流,再根据磁盘缓存策略,选择是只保存原尺寸图片,还是把重采样或者变换后的图都保存在磁盘缓存中。
移除过程: 当SingleRequest结束或者异常终止,会调用到对应EngineResource的release()方法,与acquire对应,引用计数-1,当计数为0时,会把它从活跃对象hash表移除,再添加到LruCache中。

Glide的bitmap池复用

通过引用计数的方式,有引用+1,当在目标view上调用clear,或者目标view加载了其他图,这个资源Resource会被-1。 当引用计数=0时,会丢进lruMemoryCache,同时bitmap对象会被丢进bitmapPool中。
Glide后面会根据图片配置,还有尺寸选择相应的池中对象去复用。池是固定大小的,lru算法维护,当超过一段时间没有复用,会被删除并且调用bitmap的recycle方法。
所以要注意:如果使用Glide加载图片,因为种种原因错误的持有了Glide从缓存池移除并Recyle了的bitmap,会导致崩溃。
举一个例子: 使用Glide给ImageView加载了一个图之后,先调用ImageView.getBitmapDrawable()获取这bitmap对象。
然后调用imageview.clear(),Glide会清除这个引用丢进Bitmap池,当这个缓存的bitmap被Glide策略移除,我们又拿上面获取的bitmap对象做了什么操作的话,会导致Cannot draw a recycled Bitmap发生。

Glide 对图片格式的判断方法

它是通过图片封装格式中的字节数来判断图片的类型的

  • JPEG 的前两个 Byte 为 0xFFD8
  • PNG 的前 4 个 Byte 为 0x89504E47
  • GIF 的前 3 个 Byte 为 0x474946
  • WEBP 的判定较为复杂 可以对照代码自行查看

Retrofit

1. 首先构造Retrofit对象

1
2
3
4
5

Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://api.github.com/")
.addConverterFactory(GsonConverterFactory.create())
.build();

使用的建造者模式。
在调用build()方法之后,已经传入了结果转换器还有baseUrl,此时会根据当前平台类型 Platform.get()来进行一系列初始化操作。 如果是android,就会创建一个Android类。

在Android类中,定义一个call转换器工厂。我们可以添加自己的call转换器。callAdapter其实是为了将OkHttp3的call对象进行我们需要的定制化包装。 同时会创建一个默认线程池MainThreadExecutor。

现在Retrofit对象构建完成,有了一个ExecutorCallAdapterFactory,这个factory的get()方法会返回一个CallAdapter。

2. 通过注解定义网络请求的Api,并且创建了请求服务的实体对象。

1
2
3
4
5
6
public interface GitHubService {
@GET("users/{user}/repos")
Call<List<Repo>> listRepos(@Path("user") String user);
}

GitHubService github = retrofit.create(GitHubService.class);

这里使用的是动态代理innovationHandler

create 方法的实现:

1
2
3
4
5
6
7
8
9
10
11
public <T> T create(final Class<T> service) {
// 省略非关键代码
return (T) Proxy.newProxyInstance(service.getClassLoader(),
new Class<?>[] { service },
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object... args) throws Throwable {
// 先省略实现
}
});
}

由我们传入的interface 动态代理出一个对象,代理对象就是service实体。它实现了请求api的接口。

3. 调用请求接口:

实际就干了这三件事:

1
2
3
ServiceMethod serviceMethod = loadServiceMethod(method);
OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args);
return serviceMethod.callAdapter.adapt(okHttpCall);

1. 创建ServiceMethod

使用了单例模式+缓存的方式,每一个api接口都对应的一个单例ServiceMethod。注意,构建的时候传入的是class对象。
ServiceMethod的成员包括:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ServiceMethod(Builder<T> builder) {
this.callFactory = builder.retrofit.callFactory();
this.callAdapter = builder.callAdapter;
this.baseUrl = builder.retrofit.baseUrl();
this.responseConverter = builder.responseConverter;
this.httpMethod = builder.httpMethod;
this.relativeUrl = builder.relativeUrl;
this.headers = builder.headers;
this.contentType = builder.contentType;
this.hasBody = builder.hasBody;
this.isFormEncoded = builder.isFormEncoded;
this.isMultipart = builder.isMultipart;
this.parameterHandlers = builder.parameterHandlers;
}

callFactory:创建网络请求的客户端,不指定默认为okhttp3.okHttpClient
也可以在创建时指定。
callAdapter: 负责吧okhttp3的call
适配为我们的retrofit的call 也就是默认的ExecutorCallBackCall
responseConverter: 将返回结果转化为我们想要的javabean
parameterHandlers: 负责解析api定义时的参数,并在构造http请求的时候填入参数。

2. 创建okHttpCall

同步 execute
异步 enqueue(callBack callback)

a 创建okhttp3.call

由parameterHandlers 构建请求体request
由callFactory.newCall(request)构建okhttp3请求(当然也可以由过时的HttpUrlConnection构建)

b 同步请求网络并转换结果

okhttp3.Call.execute()来同步执行网络请求
将结果用responseConventer转换为javabean

c 异步请求

okhttp3.call.enqueue(callback callback)来异步执行网络请求
回调callback接口

3. callAdapter 将okhttpCall 转换为我们想要的call类型

比如我们常用的RxJavaCallAdapter 会吧请求转化为一个Observable

看一下RxjavaCallAdapter

会将OkhttpCall 适配为Observable

  1. 根据同步还是异步(默认是异步请求 isAs = false) 创建了两个OnSubscribe
1
2
3
OnSubscribe<Response<R>> callFunc = isAsync
? new CallEnqueueOnSubscribe<>(call)
: new CallExecuteOnSubscribe<>(call);
  1. 以这个OnSubscribe对象来构建Observable
  2. 当被订阅时,Onsubscribe对象开始发送事件,call方法会被调用
1
2
3
4
5
6
7
8
9
10
11
12
13
public void call(){
...
Response<T> response;
try {
response = call.execute();
} catch (Throwable t) {
Exceptions.throwIfFatal(t);
arbiter.emitError(t);
return;
}
arbiter.emitResponse(response);

}

里面阻塞调用了okhttp3.call 的execute()方法获取网络请求结果

  1. 开始网路请求

    调用了请求接口,经过转换器 将call转换为了我们要的被观察者。
    当调用了subscribe()方法的时候,开始触发网络请求

1
2
3
4
5
6

Observable.subscribe//触发 API 调用的执行;
CallOnSubscribe(call),//clone call,创建并设置 producer;
RequestArbiter(request)//subscriber 被设置了 producer 之后最终调用 request,在 request 中发起请求,把结果发给下游;
OperatorMapResponseToBodyOrError$1(onNext)//把 response 的 body 发给下游;
最终就到了我们 subscribe 时传入的回调里面了;

retrofit里丰富的设计模式:

  1. 建造者模式: 在创建Retrofit客户端的时候,通过builder()构建者模式将需要的一些适配器创建并赋值。
  2. 适配器模式: retrofit在Android版本上默认是用okhttp3来请求网络的,okhttp3的网络请求okhttp3.call被转换为OkHttpCall,这个call.enqueue 是在子线程调用,callback的回调也是在子线程,为了在主线程操作这些回调我们势必要Handler来切换线程,但是这个okHttpCall 肯定是不适用java1.8 或者ios 等平台的,而且我们如果想使用RXjava的流式调用,okhttpCall肯定还要做进一步的适配,为了隔离网络请求接口的平台不一致性,retrofit将一些共有属性抽取出来,通过我们自定义的CallAdapter 隔离各种平台和网络请求框架的具体实现差异,适配成我们想要的方式。
  3. 静态代理:默认的CallAdapter是AndroidCallAdapter,为了把okhttp3.call 封装为子线程请求,主线程回调,使用了静态代理方式 ExecutorCallbackEnqueue() 将okhttp3.call 代理为OkHttpCall。
  4. 动态代理: 在我们调用对应的网络请求接口的时候,使用了动态代理,在调用方法的地方,动态的获取注解信息,拼装为完整的请求体,同时将返回的数据适通过我们自定义的转换器,比如讲xml 或者json转为javabean,同时将请求call 通过callAdapter适配为我们想要的格式,比如如果是RxJavaAdapter 就适配为Observable 如果不定义,就适配为OkhttpCall。

OkHttp3

-w500

1. 创建HttpClient对象

OkHttpClient client = new OkHttpClient(){
new Builder();}

2. Builder里做了什么

1 构造了Dispacher
2 设置了链接超时时间和读写超时时间

3. 创建request

通过url method body header 构建Request对象
1
2
3
Request request = new Request.Builder()
.url(url)
.build();

创建RealCall对象

client.newCall(request)
根据request构建一个RealCall对象

1
new RealCall(this, request);

同步请求网络 call.execute()

call.execute()
其实调用的RealCall.execute()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override public Response execute() throws IOException {
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed"); // (1)
executed = true;
}
try {
client.dispatcher().executed(this); // (2)
Response result = getResponseWithInterceptorChain(); // (3)
if (result == null) throw new IOException("Canceled");
return result;
} finally {
client.dispatcher().finished(this); // (4)
}
}

先判断call 是否执行过,每个call、只能执行一次
其次 会调用dispatcher.executed(this); 这个dispatcher是个请求分发器,
同步调用的时候没起到什么特殊作用,只是通知一下 请求开始了(executed),请求结束了(finished),
它更多的是用在异步调用的时候,处理多个请求的异步调度。

然后getResponseWithInterceptorChain() 是真正开始网路请求并处理结果的方法

最后 调用dispatcher 通知其已经完成请求。

Dispatcher是网络请求任务的分发器。 当决定异步请求时,Dispatcher会调用enquene(new AysncCall()) 将网络请求任务添加到请求队列中。

Dispatcher中使用了一个核心线程为0 超时时间为60s的无上限线程池。
任务队列是一个FIFO的阻塞队列来实现。
在Dispatcher中有三个链表,

readyAsyncCalls记录等待执行的异步请求。
runningAsyncCalls记录正在执行的异步请求
RunningSyncCalls记录正在执行的同步请求。

dispatcher的线程池配置 只有一个核心线程,而且任务队列长度只有0,那就是每次有请求,就先看有无空闲线程,有就交给线程执行,否则创建新的线程。

当执行call.enqueue()方法准备发起网路请求的时候,
首先判断 runningAsyncCalls队列中是不是没有超过最大请求数64 且同一个请Host下的请求不超过5,如果满足,加入runningAsyncCalls队列,否则加入readyAsyncCalls队列。

依次从runningAsyncCalls队列表头取出call任务,加入线程池执行。
通过getResponseFromInterceptorChain()去发起网络请求并获取response,这个方法被trycatch包裹,成功就回调callback的onResponse,注意此时并没有切换线程,所以okhttp的回调是在线程池中执行的。

如果失败,进入catch块,并回调onFailure();

在finally块中,移除当前的runningCalls,并扫描runningCalls链表,取出下一个等待执行的call加入线程池执行。

1
2
3
syncornized(this){
promoteCalls();
}

可以看到 不管是同步 还是异步,都是调用ResponseWithIntercepotionChain()去真正开始网络请求并获取response

getResponseWithInterceptorChain 请求网并处理数据

责任链模式 每个拦截器被串联在一起 各自处理各自能处理的工作,并将工作流向下传递。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
List<Interceptor> interceptors = new ArrayList<>();
interceptors.addAll(client.interceptors());
interceptors.add(retryAndFollowUpInterceptor);
interceptors.add(new BridgeInterceptor(client.cookieJar()));
interceptors.add(new CacheInterceptor(client.internalCache()));
interceptors.add(new ConnectInterceptor(client));
if (!retryAndFollowUpInterceptor.isForWebSocket()) {
interceptors.addAll(client.networkInterceptors());
}
interceptors.add(new CallServerInterceptor(
retryAndFollowUpInterceptor.isForWebSocket()));

Interceptor.Chain chain = new RealInterceptorChain(
interceptors, null, null, null, 0, originalRequest);
return chain.proceed(originalRequest);
}

它把实际的网络请求、缓存、透明压缩等功能都统一了起来,每一个功能都只是一个 Interceptor,它们再连接成一个 Interceptor.Chain,环环相扣,最终圆满完成一次网络请求。

-w350

这个责任链机制非常巧妙

先是定义了一串的Interceptor,每个Interceptor处理特定任务。Interceptor是一个接口 看代码就明白了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public interface Interceptor {
Response intercept(Interceptor.Chain var1) throws IOException;

public interface Chain {
Request request();

Response proceed(Request var1) throws IOException;

@Nullable
Connection connection();

Call call();

int connectTimeoutMillis();

Interceptor.Chain withConnectTimeout(int var1, TimeUnit var2);

int readTimeoutMillis();

Interceptor.Chain withReadTimeout(int var1, TimeUnit var2);

int writeTimeoutMillis();

Interceptor.Chain withWriteTimeout(int var1, TimeUnit var2);
}
}

chain里提供了我们创建一个网络请求所需要的所有方法。它的唯一实现类是Realchain
还记得上面我们开始获取response的方法吗:

1
2
3
Interceptor.Chain chain = new RealInterceptorChain(
interceptors, null, null, null, 0, originalRequest);
chain>processed(originRequest)

这里我们构造chain的时候,传入的初始index是0。
当调用到processed方法时,会创建一个新的chain并将index+1 取出传入的interceptor列表的第一项。将chain传递给它处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec, RealConnection connection) throws IOException {
if (this.index >= this.interceptors.size()) {
throw new AssertionError();
} else {
++this.calls;
if (this.httpCodec != null && !this.connection.supportsUrl(request.url())) {
throw new IllegalStateException("network interceptor " + this.interceptors.get(this.index - 1) + " must retain the same host and port");
} else if (this.httpCodec != null && this.calls > 1) {
throw new IllegalStateException("network interceptor " + this.interceptors.get(this.index - 1) + " must call proceed() exactly once");
} else {
RealInterceptorChain next = new RealInterceptorChain(this.interceptors, streamAllocation, httpCodec, connection, this.index + 1, request, this.call, this.eventListener, this.connectTimeout, this.readTimeout, this.writeTimeout);
Interceptor interceptor = (Interceptor)this.interceptors.get(this.index);
Response response = interceptor.intercept(next);
if (httpCodec != null && this.index + 1 < this.interceptors.size() && next.calls != 1) {
throw new IllegalStateException("network interceptor " + interceptor + " must call proceed() exactly once");
} else if (response == null) {
throw new NullPointerException("interceptor " + interceptor + " returned null");
} else if (response.body() == null) {
throw new IllegalStateException("interceptor " + interceptor + " returned a response with no body");
} else {
return response;
}
}
}

在interceptor的intercept方法中,又会调用这个chain的processed方法

processed方法又会以旧参数和index+1 再创建新chain。并取出interceptpor链表的第二项来处理chain。
一次类推直到链表中没有新的interceptor为止。

这样就串联成了一个调用链。

最终返回response给我们。

总结

构造OkHttpClient 并初始化分发器

1
2
3
4
OkHttpClient client = new OkHttpClient(){
Dispatcher dispatcher = new Dispatcher();构建dispatcher分发器
Long mConnectionTimeout = CONNECTION_TIME_OUT 设置链接超时时间之类的
};

构造Request对象

使用建造者模式 构建一个Request对象

1
Request request = Requset.Builder().url("https://baidu.com").build();

分发器决定同步还是异步执行

  1. 同步
1
2

client.newCall(request).execute();
  1. 异步
1
2
3
4
5

client.newCall(request).enqueue(new Callback{
void onFailure(Call call,IoException e){}
void onResponse(Call call,Response response){})
});

可以看到 首先 构建了一个请求RealCall对象。
通过RealCall的execute() 和enqueue(callback)分别实现了同步请求和异步请求。

同步请求和异步请求是由dispatcher实现的。
分发器分发见上文。最终是通过
getResponseFromInterceptorChain(Request,0,0);来实现网络请求和获取response的。

自定义拦截器请求网络并获取response

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private Response getResponseWithInterceptorChain() throws IOException {
// Build a full stack of interceptors.
List<Interceptor> interceptors = new ArrayList<>();
interceptors.addAll(client.interceptors());
interceptors.add(retryAndFollowUpInterceptor);
interceptors.add(new BridgeInterceptor(client.cookieJar()));
interceptors.add(new CacheInterceptor(client.internalCache()));
interceptors.add(new ConnectInterceptor(client));
if (!retryAndFollowUpInterceptor.isForWebSocket()) {
interceptors.addAll(client.networkInterceptors());
}
interceptors.add(new CallServerInterceptor(
retryAndFollowUpInterceptor.isForWebSocket()));

Interceptor.Chain chain = new RealInterceptorChain(
interceptors, null, null, null, 0, originalRequest);
return chain.proceed(originalRequest);
}
  1. 先把我们创建client的时候,自定义的拦截器加载到拦截器列表中
  2. 添加各种必要拦截器
  3. 构建RealInterceptorChain()
  4. 通过责任链机制,链式处理请求并返回response

Interceptors

HttpCodec

对http请求过程中的数据流进行读写操作

StreamAllocation

newStream 创建httpCodec
realStream.connection 开启socket链接,返回Connection对象

注: HttpCodec 用来读写网络请求的输入输出流、
RealCall 代表一次网络请求 对一次网络请求的封装 其实就是请求了一个特定的api
StreamAllocation 一次真实网络请求的开销 与RealCall对应 要知道 我们访问一个特定api的时候 可能会出现域名重定向,一次请求会发生多次跳转,每次跳转都对应着一次真实的网络请求的开销。 StreamAllocation就是描述这个开销的。
Connection 对应一个连接。 我们知道http请求 是基于TCP协议的,TCP是面向连接的协议,就是每次请求网络 都必须先建立连接,而建立连接需要三次握手,如果同时请求次数很多,每次都要先建立连接 再去请求网络 会造成很大的网罗开销。 为了复用连接,在Http1.1 协议中 默认打开了keep-alive,这样服务器收到这种请求之后,返回完数据并不会直接关闭连接,而是会等待一段时间,在此之间的请求 可以直接复用这个连接,就达到请求复用的效果。但是http1.1 对同时请求的数量有个最大值限制,超过这个限制的请求会被阻塞。
而且 http1.1 并不会对请求首部(请求头)进行压缩,有时候带有多个cookie的情况下 头的大小甚至可以达到kb级,流量开销还是挺大的。 为了解决多路复用和节省流量 出现了新的http2.0协议。
Http2.0的主要目标:
降低延迟,客户端的单连接单请求,server的FIFO响应队列都是延迟的大头。http最初设计都是客户端发起请求,然后server响应,server无法主动push内容到客户端。压缩http header,http1.x的header越来越膨胀,cookie和user agent很容易让header的size增至1kb大小,甚至更多。而且由于http的无状态特性,header必须每次request都重复携带,很浪费流量。

Okhttp使用链接池 实现了链接的多路复用机制。

RetryAndFollowUpInterceptor

获取ConnectPool 连接池 连接池内使用一个双端队列描述链接Deque,同时维护了一个路由黑名单表。
每个链接 是一个RealConnect对象,RealConnection对象是对socket链接的封装。
链接池主要提供了

  1. 提供一个可复用的链接
  2. 将链接加入连接池
  3. 清除无用链接
  4. 清除重复链接
    等方法。

在重定向拦截器中,我们将请求url封装为一个Address对象。Address对象描述的是服务器地址,将这个Address对象作为参数 构造StreamAllocation对象。

要记住,StreamAllocation代表一次网络请求开销,是一次call的数据流动,他可以复用一个已经建立的链接。

重定向拦截器 构造了一个StreamAllocation,但是并没有开始网络请求。主要是为了创建一个StreamAllocation供后面的拦截器使用。
在创建了StreamAllocation之后,调用了

1
((RealInterceptChain)chain).proceed(request,streamAllocation,null,null);

将request和streamAllocation传递给下一个Inrerceptor去处理了。

后面的拦截器会判断缓存策略,发起网络请求,获取response。

获取response之后,重定向拦截器判断是否需要重新请求或者重定向到其他地址,最大重定向层级30

BridgeInterceptor

重定向拦截器下面的拦截器。
请求会被发送到BridgeInterceptor。这个拦截器
主要是为了完善请求头,如果调用者没有写,就自动帮我们加上对应的Content-Type Content-Length
User-Agent之类的

CacheInterceptor

由桥接拦截器传递过来。主要是实现相应的缓存策略。

  1. 根据request的请求头 判断是强制缓存还是对比缓存
    如果带有max-age = *** 表示使用的是强制缓存
    如果带有的if-none-match(etag)或者 if-modify-since(last-modify)表示是对比缓存
  2. 根据缓存策略 创建 networkRequest 和 cachedResponse对象。
  3. 如果是强制缓存,networkRequest就为null 此时分两种情况:

    a. 无缓存可用 cachedResponse为null 直接构建新的response 返回码是504 报错
    b. 有缓存 直接返回cacheResponse

  4. 如果是非强制缓存 将request交给下游的interceptor去处理 请求网络并获取服务器的返回结果,由服务器的返回结果来判断 是使用缓存 还是再次请求网络从服务器中获取

  5. 将服务器返回的数据 赋值给networkResponse 判断返回码是否为304
  6. 如果是304 使用cachedResponse 获取缓存的response并返回
  7. 否则 再次请求网络 获取数据
  8. 将完整的response 缓存在cache文件中 返回此次的response。
  9. cacheWritingResponse()写入缓存 通过Okio以及DiskLruCache来实现
  10. 删除无效缓存。

connectInterceptor

由缓存拦截器判断之后,如果缓存失效,则将重新请求网络,此时会将request往下传递给connectInterceptor。

链接拦截器只完成了从连接池中查找是否有可以复用的链接,如果没有 就创建链接并添加到连接池的作用。

查找可用链接的真正实现是有上文的重定向拦截器创建的。

1
RealConnection connection = streamAllocation.findConnection(connectionPool,address,null,null);

StreamAllocation.findConnection(),使用address来从连接池中获取一个可复用的链接,如果没有就构造一个新的并加入到链接池中,

connection描述了请求的地址

  1. Internal.getInstance.get(connectionPool,address)直接根据地址匹配,如果address跟连接池中的adderss完美匹配,直接返回这个链接对象。
  2. 地址不匹配,查看有无设置过路由,有的话再尝试从链接池中获取链接对象 Internal.getInstance.get(connectionPool, address,route)。
  3. 如果还没有找到可以复用的链接,就直接创建一个新的链接。并将链接的stream计数器+1,添加到连接池。 这里涉及到连接池如何判断链接是否失效的问题。因为每个StreamAllication对应链接上的一个数据流动,如果链接上没有数据流了,那表明当前链接是闲置状态,当闲置超过5分钟,就会被回收。
  4. 查看链接池中有无多余重复链接result.isMultiplexed(),有就删除Internal.instance.deduplicate。

connectionInterceptor 获取可用的链接之后,往下传递给CallServerInterceptor

CallServerInterceptor

这是一个真正发起网络请求的拦截器。所以他也是在拦截器链表的对尾。

建立连接 ConnectInterceptor
找到一个可用的 RealConnection,再利用 RealConnection 的输入输出(BufferedSource 和 BufferedSink)创建 HttpCodec 对象,供后续步骤使用。这个HttpCodec 其实使用Okio包 对 Socket 的读写操作进行封装。是对http的抽象。

发送和接收数据:CallServerInterceptor
使用上面创建的httpcodec来发起和接收数据:
向服务器发送 request header;
如果有 request body,就向服务器发送;
读取 response header,先构造一个 Response 对象;
如果有 response body,就在 3 的基础上加上 body 构造一个新的 Response 对象;

okhttp 使用websocket

websocket 是基于tcp协议基础上的,和http协议类似,是一种新的网络通信协议,是全双工信道,就是服务器可以推数据给客户端,客户端可以发数据给服务器。
websocket是按照数据帧来传递数据的

1
2
3
4
5
OkhttpClinet client = new OkhttpClient();
Request request = Request.builder.url("ws://fdedee").build;
client.newWebsocket(request,new WebsocketListener{

})

okhttp的连接池复用:

StreamAllocation会从连接池中寻找当前Request(OkHttp会对每一个请求RealCall 生成多个Request对象,因为一个Call请求,可能会被多次重定向,会发生多次链接。)可以复用的RealConnction对象。
OkHttp使用ConnectionPool来维护连接池。

  • Deque 双端队列来保存链接队列
  • ThreadPoolExecutor 用来清理和维护链接对象。
  • OkHttp使用了一个 核心线程数为0,最大线程数无上界,等待队列为SynchronousQueue(这个队列是无容量的,每当往这个队列插入数据时,会直接去线程池中寻找可用线程, 如果没有线程会立刻创建一个线程用来执行任务。所有线程超时60s会被回收)。
  • 什么时候执行这清理任务? 在每次往连接池中丢入新的链接时,会判断是不是在清理,如果没有要执行一次清理。
  • 按照什么标准把无用链接清除掉?遍历链接Deque,找出该链接的弱引用为0的链接,判定为空闲链接,对于空闲链接,找出空闲时间最长的链接,检查是否到了设置的keep-alive时长,或者是不是已经到了设置的最大空闲链接数,如果到了则将这些空闲链接关闭并清除。

  • 如何判断一个链接可以被复用呢? 根据传入的Address和Router配置,去遍历连接池Deque,找到符合复用条件的链接。这个复用条件除了地址和路由匹配外,还需要ssl host等相匹配。

okhttp的链接池清除机制:

okhttp对一次call产生的多个steamallocation对象(比如可能多次的网络真实开销),偏向于用同一个connection对象去描述,同事每一个streamallocation对应一次真实的物理请求,内部使用socket来表示。
okhttp会经可能的对连接进行复用,同时维护了一套策略去清理无效链接。

  • 外层一个死循环 每次判断一下上次循环返回的时间
  • 遍历deque里的所有连接。检查这个链接是否产生了泄漏(realconnection中的streamallocation 超过了5个 并且keepalive时间超过5min 会被判定为泄口),是的话 直接从队列中删除,同时返回一个0s表示立刻重新循环清理。
  • 如果目前还可以塞得下5个链接(连接池最大保存5个空闲链接,最长存活时间5min),要看有没有哪些链接的keepalive是即将到5min的。返回还剩多少时间。当循环执行到返回的这段时长,会再次清理。
  • 如果目前全部是活跃的链接(如何看是不是活跃? 看realconnection里的list<weakreference> 删除为null的引用,看list里数量是不是为0 不是的话表示当前连接还在被使用。)
  • 如果一个活跃的链接都没有 返回-1 跳出死循环、
  • 每次创建一个新的connection时 会执行一遍清理

okhttp如何判断一个链接是健康的

1、socket已经关闭
2、输入流关闭
3、输出流关闭
4、如果是HTTP/2连接,则HTTP/2连接也要关闭。

okhttp如何判断一个connection可复用

如果连接达到共享上限,则不能重用
非host域必须完全一样,如果不一样不能重用
如果此时host域也相同,则符合条件,可以被复用
如果host不相同,在HTTP/2的域名切片场景下一样可以复用

okhttp的无上界线城池

。他这样设计成不设上限的线程,以保证I/O任务中高阻塞低占用的过程,不会长时间卡在阻塞上。

okhttp 三个重要的类:

参考该链接

apk前签名机制:

apk签名

签名之后 会生成三个文件


1
2
3
MANIFEST.MF
CERT.SF
CERT.RSA

1.MAINFEST.MF

逐一遍历里面apk项目中的所有条目,如果是目录就跳过,如果是一个文件,就用SHA1(或者SHA256)消息摘要算法提取出该文件的摘要然后进行BASE64编码后,作为“SHA1-Digest”属性的值写入到MANIFEST.MF文件中的一个块中。该块有一个“Name”属性,其值就是该文件在apk包中的路径。

2. CERT.SF

  1. 计算上面MANIFEST.MF文件的整体SHA1值,再经过BASE64编码后,记录在CERT.SF主属性块(在文件头上)的“SHA1-Digest-Manifest”属性值值下

【校验上面文件整体的完整性】

  1. 逐条计算MANIFEST.MF文件中每一个块的SHA1,并经过BASE64编码后,记录在CERT.SF中的同名块中,属性的名字是“SHA1-Digest
    【校验上面文件每个条目的完整性】

主要是为了教研上面生成的CERT.SF 文件的合法性

3. CERT.RSA

这里会把之前生成的 CERT.SF文件, 用私钥计算出签名(算出cret.sf 的sha1值,然后用私钥进行非对称加密), 然后将签名以及包含公钥信息的数字证书一同写入 CERT.RSA 中保存。CERT.RSA是一个满足PKCS7格式的文件。

首先,如果你改变了apk包中的任何文件,那么在apk安装校验时,改变后的文件摘要信息与MANIFEST.MF的检验信息不同,于是验证失败,程序就不能成功安装。
其次,如果你对更改的过的文件相应的算出新的摘要值,然后更改MANIFEST.MF文件里面对应的属性值,那么必定与CERT.SF文件中算出的摘要值不一样,照样验证失败。
最后,如果你还不死心,继续计算MANIFEST.MF的摘要值,相应的更改CERT.SF里面的值,那么数字签名值必定与CERT.RSA文件中记录的不一样,还是失败。
那么能不能继续伪造数字签名呢?不可能,因为没有数字证书对应的私钥。
所以,如果要重新打包后的应用程序能再Android设备上安装,必须对其进行重签名。

安装验证

主要看PackageParser.java 负责解析apk文件的签名信息

  1. 验证Apk中的每个文件的算法(数据摘要+Base64编码)和MANIFEST.MF文件中的对应属性块内容是否配对
    依次遍历每个文件,算出其数据指纹(会先读取MAINFEST.MF文件中的头字段 这里是”SHA1-Digest“),根据对应的加密方法使用对应的加密算法算出数据指纹,并进行base64转换,与MANIFEST.MF 的各个字段值进行验证。如果不通过,会抛出”INSTALL_PARSE_FAILED_NO_CERTIFICATION“异常,应用安装会终止。

  2. 验证CERT.SF文件的签名信息和CERT.RSA中的内容是否一致

主要调用JarUtils.verifySignature()方法,从rsa文件中获取签名信息,保存在证书数组里,(安卓apk允许对应用进行多次签名)并且验证CERT.SF文件的签名信息和CERT.RSA中的内容是否一致。这个主要是为了保证CERT.SF文件的有效性,好继续进行下一步的动作

  1. MANIFEST.MF整个文件签名在CERT.SF文件中头属性中的值是否匹配以及验证MANIFEST.MF文件中的各个属性块的签名在CERT.SF文件中是否匹配
  2. 上面的三个方法 主要是为了保证安装的应用是未经过篡改的,在安装之前,还会再检验一下 已安装应用于即将安装的应用中的签名是否一致。如果不一致 会报错”INSTALL_PARSE_FAILED_INCONSTENT_CERTIFICATIONS“,所谓比对签名,就是比对证书中的公钥信息是否一致
  3. 我们的应用安装白名单机制,是需要sdk使用商将签名公钥提供,我们会再次使用一次rsa算法,将公钥信息进行加密后保存在系统证书白名单数据库中,私钥信息我们会写死在packageParser里,因为使用者无法拿到我们的源码,是不可能知道我们的私钥信息的,在第5步插入了自己的判断逻辑,首先获取白名单数据库中的加密签名,用私钥解密,验证应用的签名文件是否与白名单中一致,如果不是就抛出异常。”INSTALL_PARSE_FAILED_BAD_MANIFEST“

java基础:

类的初始化 (主动引用 被动引用)

什么时候会触发类的初始化:

  1. 使用new 关键字创建了一个类的实例
  2. 访问类的静态变量 注意 静态常量不会导致类的初始化,是因为jvm将常量当做值而不是域来看待,当用到了静态常量,jvm并不会为此生成字节码从对象中载入域的值,而是直接将该值插入字节码中。这是一种很有用的优化。但是静态常量一旦变化了,所有用到它的地方都需要重新编译。
  3. 调用了类的静态方法
  4. 反射
  5. 当初始化一个类发现父类还没初始化 会先初始化父类
  6. 虚拟机启动时,定义了main方法的类会先初始化
    以上被称之为 主动引用 除上述几种情况外,都属于被动引用
    被动引用不会触发类的初始化
  7. 子类调用父类的静态变量 子类不会初始化
  8. 通过数组定义来引用类,不会初始化 SuperClass[] classes = new SupercClass[1];不会触发初始化
  9. 访问类的常量(final),不会初始化

    初始化次序:

    父类–静态变量
    父类–静态初始化块
    子类–静态变量
    子类–静态初始化块
    子类main方法
    父类–变量
    父类–初始化块
    父类–构造器
    i=9, j=0
    子类–变量
    子类–初始化块
    子类–构造器
    i=9,j=20

class文件从解析到生成对象的全过程:

1. java编译器将java文件编译为class文件

class文件是一组以8位字节为基础的二进制流,各个数据项严格按照一定的次序和规则排列在class文件中,没有任何的分隔符,所有的内容都是java程序运行时的必要数据项。由上到下依次为:

  1. 头四个字节: 魔数 + class文件版本
  2. 常量池:字面量(各种字符串,final修饰符修饰的常量值。)和符号引用(类和接口的全限定名,字段的名称和描述符,方法的名称和描述符)
  3. 访问标识, 常量池之后的两个字节。标识当前是类还是接口 声明的是public 还是private abstract final之类
  4. 类索引(当前类的全限定名) 父类索引(父类的全限定名,除了object没有父类 其他都有) 接口索引(可以多继承接口,所以这是一个数组)
  5. 字段表集合: 是类字段还是实例字段(static),public 还是static 是不是volatile 是不是final。
  6. 方法表集合
  7. 属性表集合
  8. 指令码集合

    2. jvm运行时数据区

java程序运行的时候,jvm会把它管理的内存划分为以下几种数据区

  1. java堆 所有线程共享,所有实例化的对象和数组都会被放在堆中。
    对象中的内存布局:
    a. 对象头:
    对象本身的信息:锁信息 根据是偏向锁 轻量级 重量级锁在该区域存储的数据不同。
    gc分代年龄、
    哈希码
    
    类型信息: 指向该对象对应的唯一class对象指针。
    b. 对象数据
    程序中定义的各种类型的字段内容 指向的是方法区中对应的信息

c. 对其区域
不够8个字节要对齐成8个字节。

  1. 方法区:类信息,常量,静态变量,jit编译器编译后的代码 常量池也是在方法区中的。对象头和方法区是线程共享的。
    下面是每创建一个线程 都会创建的数据区
  2. vm栈 每个线程都会创建自己的vm栈。每调用一个方法 都会创建一个方法栈帧加入到vm栈中
    一个栈帧的数据结构:
    a. 局部变量表 方法中定义的局部变量数据
    b. 操作数栈 存放方法执行过程的中间变量
    c. 动态链接 指向该栈帧对应的常量池地址
    d. 返回地址 方法调用完需要返回时,需要回到调用它的方法的地址。这个就是存储这种信息的。

  3. native方法栈

  4. 程序计数器: 记录下个指令的位置。

java 集合相关:

hashMap

线程不安全的集合。fail-fast机制,内部有一个volitile类型的变量记录被修改次数,当进行插入操作发现该数据与当前不一致会抛出异常。
采用数组+Entry链表实现。默认初始化长度是16,扩容因子是0.75 每次扩容按照2的倍数进行扩容,而且数组长度也是2的倍数。 当查找数据的时候,首先计算插入数据key的hash值,然通过hash值与(length-1)进行与运算,求得在数组中的位置。这样比直接对数组长度求模要高效很多
index = h &(length-1)
当插入数据已存在 直接覆盖插入,否则插入在entry链表表头

注: jdk1.8中 hashMap有了重大更新

为了解决hash碰撞导致的Entry链表过长的情况导致查询效率低,jdk1.8 引入了红黑树。

  1. 没有冲突,数据存储在数组中
  2. 有冲突 且Entry链表长度不超过8 将数据存储在单链表中
  3. 当Entry长度超过8 将吧Enrty单链表转化为 红黑数
  4. 红黑树 的查找和插入时间复杂度均为o(ln n) 单链表是n

concurrentHashMap

线程安全的hashMap 数组部分改由Segment实现,继承自并发包的ReentrainLock,实现了分段锁机制。
每个segment维护一个特殊的HashEntry链表,读可以并发读取无需加锁,写是加锁的。

  1. HashEnrty的next是final类型,不能在中间操作成员,每次put只能放在表头。hashEnrty对象的不变性,降低了读对锁的要求
  2. value字段是volitine修饰,是线程可见,每次读取都是最新值。
  3. 如果父读的过程中发生了指令重拍现象,则加锁重读。

理想状态的并发级别是16个线程,每个Segment守护大约为总桶数的1/16

hashSet

和hashMap基本一样 只不过存的时候我们只需要传key就可以,默认会放一个object对象作为value,本质还是hashMap 实现的,初始还是16 扩容系数0.75. 如果传入的key已经存在 直接返回false。

hashTable

线程安全的hashMap 已经淘汰,推荐使用concurrentHashMap。默认长度11 扩容因子0.75 每次扩容 长度为2倍+1。所有的public方法用syncornized包裹,所以线程安全。
不推荐使用。

LinkedHashMap

非线程安全。排序的HashMap,Entry链表用的是双向链表,每个节点增加了before和after成员,构成双向链。单独增加了一个Header节点,记录链表头。是根据时间排序的。可以设置为 是插入时排序 还是取值时排序。 会将最近使用的元素放在Header节点的后一个节点,这种特性使得linkedHashMap可以作为LruCache的存储数据结构来使用,并且提供了一个eldest()方法获取最不常用的元素。

LinkedHashSet

同上

ArrayList

数组实现的列表,自动扩容,可被扩容,支持序列化。无参构造方法默认长度为10,扩容倍数为原来一半+1个。

Vector

和ArrayList类似 也是基于数组实现的动态可扩容列表=,是线程安全的,每次扩容数据增大为原来的1倍

LinkedList

双向链表实现的动态列表,插入 删除比ArrayList快,但是随机访问比Arraylist慢,因为Arraylist是基于数组的,

SparseArray

用双数组实现,key只能存int类型,key用一个数组存放,value也用一个数组存放,每次put元素都会进行二分法查找,将其排好序再插入,查找的时候,也会使用二分法查找,但是因为其插入,查找 删除都需要进行二分法查找,当数据量很大的时候,效率将低50% 。 因为hasMap每次扩容的时候,都需要将容量扩大一倍,同时对所有数据再hash,这个开销非常大,所以当 数据量低于1000,且key为int类型时,使用SparseArray将获得很好的性能。

CopyOnWriteArrayList

读写锁,读不加锁 写加锁 适合多度少写的使用情况。

虚拟机相关 JVM–> Divalk 虚拟机 –> ART虚拟机

JVM

一个java虚拟机 必须包括 类加载器,解释执行,垃圾回收 三个部分。

1. 编译java代码

首先是通过javac java编译器 将java代码编译为信息密度高的java字节码 变成class文件
class文件的文件格式:

名称 数量 描述
U4 Magic 1 魔数 标识该文件为class文件
U2 minor_version 1 次版本
U2 Major_version 1 主版本
U2 constant_pool_count 1 常量池数量(一个class文件只有一个常量池)
cp_info Constant_pool 上面-1 常量池(数据类型有11种,真正只有utf8,其他都是对其的引用)
u2 Access_flags 1 类的访问标记
u2 This_class 1 记录当前类的全限定名
u2 super_class 1 父类的全限定名
u2 Interface_count interface_count 接口数量
u2 Interfaces 1 接口全限定名
u2 Fields_count 1 成员数量
field_info Fields fields_count 成员信息
U2 Methods_count 1 方法数量
Methods_info Methods methods_count 方法信息
U2 Attrtibutes_count 1 属性数量
Attrtibutes_info Attrtibutes attr_counts 属性信息

a: 常量池的类型包括:

数据类型 标记 描述
CONSTANT_Utf8 1 UTF-8编码的Unicode字符串
CONSTANT_Integer 3 int类型字面值
CONSTANT_Float 4 float类型字面值
CONSTANT_Long 5 long类型字面值
CONSTANT_Double 6 double类型字面值
CONSTANT_Class 7 对一个类或接口的符号引用
CONSTANT_String 8 String类型字面值
CONSTANT_Fieldref 9 对一个字段的符号引用
CONSTANT_Methodref 10 对一个类中声明的方法的符号引用
CONSTANT_InterfaceMethodref 11 对一个接口中声明的方法的符号引用
CONSTANT_NameAndType 12 对一个字段或方法的部分符号引用

其中 constant_uft8_info 是最基础的类型 会被其他引用

这个类型的数据是utf8格式的字符串常量 基本一个类的全部信息 都可以由字符串常量来表述了:

  • 程序中的字符串常量
  • 常量池所在当前类(包括接口和枚举)的全限定名
  • 常量池所在当前类的直接父类的全限定名
  • 常量池所在当前类型所实现或继承的所有接口的全限定名
  • 常量池所在当前类型中所定义的字段的名称和描述符
  • 常量池所在当前类型中所定义的方法的名称和描述符
  • 由当前类所引用的类型的全限定名
  • 由当前类所引用的其他类中的字段的名称和描述符
  • 由当前类所引用的其他类中的方法的名称和描述符
  • 与当前class文件中的属性相关的字符串, 如属性名等
b:访问标记:

总共7项 标记当前类 是 class 是接口 是枚举 还是注解类型 是不是public 是不是final的
注意 这是描述类层面的 不是描述方法和字段的访问类型

c:fields counts 和field

字段和字段信息
字段只包括当前类中定义的字段 不包括父类的字段。
字段信息是一个数组 每个字段信息都是一个field_info结构 信息都是对常量池中的数据的引用
包括了字段名,字段访问表示 字段类型 指向常量池的索引 以及字段的属性(标识字段是否是静态的 constant,是否过时 deprecated 以及Synthetic)

d :methods

存放的是method_info 数组 标识方法信息的
-w78

access_flags , name_index, descriptor_index 。 他们分别描述方法的访问修饰符, 方法名和方法描述符。
method_info中还有attributes_count和attributes
其中attritubes 包括方法执行过程中的所有指令:
-w184

  • attribute_name_index指向常量池中的一个CONSTANT_Utf8_info , 这个CONSTANT_Utf8_info 中存放的是当前属性的名字 “Code” 。

  • attribute_length给出了当前Code属性的长度(不包括前六字节)。

  • max_stack 指定当前方法被执行引擎执行的时候, 在栈帧中需要分配的操作数栈的大小。

  • max_locals指定当前方法被执行引擎执行的时候, 在栈帧中需要分配的局部表量表的大小。注意, 这个数字并不是局部变量的个数, 因为根据局部变量的作用域不同, 在执行到一个局部变量以外时, 下一个局部变量可以重用上一个局部变量的空间(每个局部变量在局部变量表中占用一个或两个Slot)。 方法中的局部变量包括方法的参数, 方法的默认参数this, 方法体中定义的变量, catch语句中的异常对象。 关于执行引擎的相关内容会在后面的博客中讲到。

  • code_length指定该方法的字节码的长度, class文件中每条字节码占一个字节。

  • code存放字节码指令本身, 它的长度是code_length个字节。

  • exception_table_length 指定异常表的大小

  • exception_table就是所谓的异常表, 它是对方法体中try-catch_finally的描述。 exception_table可以看做是一个数组, 每个数组项是一个exception_info结构, 一般来说每个catch块对应一个exception_info,编译器也可能会为当前方法生成一些exception_info。 exception_info的结构如下(为了直观的显示exception_info, exception_table和Code属性的关系, 画出了Code属性,的话读者就会更清楚各个数据项之间的位置关系和包含关系):

加载class字节码

jvm的内存结构
-w321

  • 通过一个类的全限定名来获取其定义的二进制字节流。
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
  • 在 Java 堆中生成一个代表这个类的 java.lang.Class 对象,作为对方法区中这些数据的访问入

为什么用Class.forName(“Test”).getMethod(“void”,int.class) 这样的写法被称之为反射呢。

因为一个类被加载之后,比如上文的Test类,Test.class文件中的常量池 会被加载在运行时数据结构中的方法区中,这些数据是线程共享的。 当类被加载之后,会生成一个Class类型对象,该对象存放在堆中,他对方法区中该类的数据做了映射,就像是一面镜子,反射着这个类的所有信息。 所以通过class对象来获取类信息的方式,被形象的称之为反射。

链接

验证
准别 为静态类变量分配默认值
解析 将符号引用替换为直接引用 (会发生在初始化前 也可能发生在初始化之后)比如静态类变量的引用 会在类被加载的时候就去查找 本类 父类 父接口是否存在 如果存在就将该静态类变量的引用替换为其在内存中的真正地址 与当前类其实已经没有关系了。

所以乳 如果子类引用了父类的静态类变量 并不会触发子类的初始化。 此时链接发生在初始化之前。

初始化

开始对成员变量进行初始化并赋值

方法的执行时序

JIT AOT JAVAC 编译器区别

javac就是我们常用的将java文件编译为class字节码的

之前的java程序在运行过程中 都是由解释器 一行行解析class字节码 一行行执行 但是这势必会存在效率问题 因为解释运行的速度不高。
后来为了优化执行速度,出现了JIT 编译器。
JIT JUST in time. 顾名思义 执行器编译。
JIT编译器会判断 在程序执行过程中 哪些代码是高频执行的,并将改代码直接翻译成与当前平台相关的机器码。下次再执行到该代码时 会直接执行机器码 速度就快了很多。

在5.0以前 divlik虚拟机 就是JIT 编译器

这个高频代码的判断 被称之为 热点探测 包括 基于采样点探测 基于计数器探测。

AOT

AOT ahead of time
在程序运行之前 就将class文件直接翻译成对应平台的字节码 这样会加快应用的启动时间,因为直接从字节码开始读取的。但是会带来内存增大的问题。而且在android平台上 还会带来意想不到的坑,比如如果在so文件中引用了libs目录下的其他so库,会爆找不到路径的错误,是因为aot编译器直接将so库与java文件直接打包成了平台对应的机器码文件aot文件。

android 历史各版本的编译器演进:

  • Android 4.x(Interpreter + JIT)原理:平时代码走解释器,但热点trace会执行JIT进行即时编译优点:占用内存少缺点:耗电(退出App下次启动还会重复编译),卡顿(JIT编译时)Android
  • 5.0/5.1/6.0(interpreter + AOT)原理: 在AOT模式下,App在安装过程时, 就会完成所有编译。优点: 性能好缺点: App安装时间长,占用存储空间多。】
  • Android 7.0/7.1的ART引入了全新的Hybrid模式(Interpreter + JIT + AOT)原理: App在安装时不编译, 所以安装速度快。在运行App时, 先走解释器, 然后热点函数会被识别,并被JIT进行编译, 存储在jit code cache, 并产生profile文件(记录热点函数信息)。 等手机进入charging和idle状态下, 系统会每隔一段时间扫描App目录下profile文件,并执行AOT编译(Google官方称之为profile-guided compilation)。不论是jit编译的binary code, 还是AOT编译的binary code, 它们之间的性能差别不大, 因为它们使用同一个optimizing compiler进行编译。优点: App安装速度快,占用存储少(只编译热点函数)。缺点: 前几次运行会较慢, 只有用户操作得次数越多,jit 和AOT编译后, 性能才会跟上来

    dv虚拟机 对比jvm虚拟机

    jvm 的每一个class文件都对应一个常量池。 但是很多常量池中的信息其实是重复的,dv虚拟机在将class文件编译为dex文件时,将所有class文件的常量池合并为一个,减少了apk的体积。

其次 jvm是基于求职栈的 dvm的指令比jvm长,是基于虚拟寄存器的。
dvm的方法栈帧中不存在 求值栈和临时变量,替代的是虚拟寄存器、常用的是v0 -v15

dvm的虚拟寄存器 每个线程都有一个 不用担心多线程 保护寄存器问题。

手画一下Android系统架构图,描述一下各个层次的作用?

Android系统架构图

从上到下依次分为六层:

  • 应用框架层
  • 进程通信层
  • 系统服务层
  • Android运行时层
  • 硬件抽象层
  • Linux内核层

Activity如与Service通信?

可以通过bindService的方式,先在Activity里实现一个ServiceConnection接口,并将该接口传递给bindService()方法,在ServiceConnection接口的onServiceConnected()方法
里执行相关操作。

Service的生命周期与启动方法由什么区别?

  • startService():开启Service,调用者退出后Service仍然存在。
  • bindService():开启Service,调用者退出后Service也随即退出。

Service生命周期:

  • 只是用startService()启动服务:onCreate() -> onStartCommand() -> onDestory
  • 只是用bindService()绑定服务:onCreate() -> onBind() -> onUnBind() -> onDestory
  • 同时使用startService()启动服务与bindService()绑定服务:onCreate() -> onStartCommnad() -> onBind() -> onUnBind() -> onDestory

Service先start再bind如何关闭service,为什么bindService可以跟Activity生命周期联动?

广播分为哪几种,应用场景是什么?

  • 普通广播:调用sendBroadcast()发送,最常用的广播。
  • 有序广播:调用sendOrderedBroadcast(),发出去的广播会被广播接受者按照顺序接收,广播接收者按照Priority属性值从大-小排序,Priority属性相同者,动态注册的广播优先,广播接收者还可以
    选择对广播进行截断和修改。

广播的两种注册方式有什么区别?

  • 静态注册:常驻系统,不受组件生命周期影响,即便应用退出,广播还是可以被接收,耗电、占内存。
  • 动态注册:非常驻,跟随组件的生命变化,组件结束,广播结束。在组件结束前,需要先移除广播,否则容易造成内存泄漏。

广播发送和接收的原理了解吗?

  1. 继承BroadcastReceiver,重写onReceive()方法。
  2. 通过Binder机制向ActivityManagerService注册广播。
  3. 通过Binder机制向ActivityMangerService发送广播。
  4. ActivityManagerService查找符合相应条件的广播(IntentFilter/Permission)的BroadcastReceiver,将广播发送到BroadcastReceiver所在的消息队列中。
  5. BroadcastReceiver所在消息队列拿到此广播后,回调它的onReceive()方法。

广播传输的数据是否有限制,是多少,为什么要限制?

ContentProvider、ContentResolver与ContentObserver之间的关系是什么?

  • ContentProvider:管理数据,提供数据的增删改查操作,数据源可以是数据库、文件、XML、网络等,ContentProvider为这些数据的访问提供了统一的接口,可以用来做进程间数据共享。
  • ContentResolver:ContentResolver可以不同URI操作不同的ContentProvider中的数据,外部进程可以通过ContentResolver与ContentProvider进行交互。
  • ContentObserver:观察ContentProvider中的数据变化,并将变化通知给外界。

遇到过哪些关于Fragment的问题,如何处理的?

  • getActivity()空指针:这种情况一般发生在在异步任务里调用getActivity(),而Fragment已经onDetach(),此时就会有空指针,解决方案是在Fragment里使用
    一个全局变量mActivity,在onAttach()方法里赋值,这样可能会引起内存泄漏,但是异步任务没有停止的情况下本身就已经可能内存泄漏,相比直接crash,这种方式
    显得更妥当一些。

  • Fragment视图重叠:在类onCreate()的方法加载Fragment,并且没有判断saveInstanceState==null或if(findFragmentByTag(mFragmentTag) == null),导致重复加载了同一个Fragment导致重叠。(PS:replace情况下,如果没有加入回退栈,则不判断也不会造成重叠,但建议还是统一判断下)

1
2
3
4
5
6
7
8
@Override 
protected void onCreate(@Nullable Bundle savedInstanceState) {
// 在页面重启时,Fragment会被保存恢复,而此时再加载Fragment会重复加载,导致重叠 ;
if(saveInstanceState == null){
// 或者 if(findFragmentByTag(mFragmentTag) == null)
// 正常情况下去 加载根Fragment
}
}

Android里的Intent传递的数据有大小限制吗,如何解决?

Intent传递数据大小的限制大概在1M左右,超过这个限制就会静默崩溃。处理方式如下:
因为binder给用户进程就是1m 给系统核心进程是4m 给serviceMnager是128kb

  • 进程内:EventBus,文件缓存、磁盘缓存。
  • 进程间:通过ContentProvider进行款进程数据共享和传递。

描述一下Android的事件分发机制?

Android事件分发机制的本质:事件从哪个对象发出,经过哪些对象,最终由哪个对象处理了该事件。此处对象指的是Activity、Window与View。

Android事件的分发顺序:Activity(Window) -> ViewGroup -> View

Android事件的分发主要由三个方法来完成,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 父View调用dispatchTouchEvent()开始分发事件
public boolean dispatchTouchEvent(MotionEvent event){
boolean consume = false;
// 父View决定是否拦截事件
if(onInterceptTouchEvent(event)){
// 父View调用onTouchEvent(event)消费事件,如果该方法返回true,表示
// 该View消费了该事件,后续该事件序列的事件(Down、Move、Up)将不会在传递
// 该其他View。
consume = onTouchEvent(event);
}else{
// 调用子View的dispatchTouchEvent(event)方法继续分发事件
consume = child.dispatchTouchEvent(event);
}
return consume;
}

image


image

伪代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* 点击事件产生后
*/
// 步骤1:调用dispatchTouchEvent()
public boolean dispatchTouchEvent(MotionEvent ev) {

boolean consume = false; //代表 是否会消费事件

// 步骤2:判断是否拦截事件
if (onInterceptTouchEvent(ev)) {
// a. 若拦截,则将该事件交给当前View进行处理
// 即调用onTouchEvent ()方法去处理点击事件
consume = onTouchEvent (ev) ;

} else {

// b. 若不拦截,则将该事件传递到下层
// 即 下层元素的dispatchTouchEvent()就会被调用,重复上述过程
// 直到点击事件被最终处理为止
consume = child.dispatchTouchEvent (ev) ;
}

// 步骤3:最终返回通知 该事件是否被消费(接收 & 处理)
return consume;

}

描述一下View的绘制原理?

View的绘制流程主要分为三步:

  1. onMeasure:测量视图的大小,从顶层父View到子View递归调用measure()方法,measure()调用onMeasure()方法,onMeasure()方法完成绘制工作。
  2. onLayout:确定视图的位置,从顶层父View到子View递归调用layout()方法,父View将上一步measure()方法得到的子View的布局大小和布局参数,将子View放在合适的位置上。
  3. onDraw:绘制最终的视图,首先ViewRoot创建一个Canvas对象,然后调用onDraw()方法进行绘制。onDraw()方法的绘制流程为:① 绘制视图背景。② 绘制画布的图层。 ③ 绘制View内容。
    ④ 绘制子视图,如果有的话。⑤ 还原图层。⑥ 绘制滚动条。

requestLayout()、invalidate()与postInvalidate()有什么区别?

  • requestLayout():该方法会递归调用父窗口的requestLayout()方法,直到触发ViewRootImpl的performTraversals()方法,此时mLayoutRequestede为true,会触发onMesaure()与onLayout()方法,不一定
    会触发onDraw()方法。
  • invalidate():该方法递归调用父View的invalidateChildInParent()方法,直到调用ViewRootImpl的invalidateChildInParent()方法,最终触发ViewRootImpl的performTraversals()方法,此时mLayoutRequestede为false,不会
    触发onMesaure()与onLayout()方法,当时会触发onDraw()方法。
  • postInvalidate():该方法功能和invalidate()一样,只是它可以在非UI线程中调用。

一般说来需要重新布局就调用requestLayout()方法,需要重新绘制就调用invalidate()方法。

Scroller用过吗,了解它的原理吗?

了解APK的打包流程吗,描述一下?

Android的包文件APK分为两个部分:代码和资源,所以打包方面也分为资源打包和代码打包两个方面,这篇文章就来分析资源和代码的编译打包原理。

APK整体的的打包流程如下图所示:
image
具体说来:

  1. 通过AAPT工具进行资源文件(包括AndroidManifest.xml、布局文件、各种xml资源等)的打包,生成R.java文件。
  2. 通过AIDL工具处理AIDL文件,生成相应的Java文件。
  3. 通过Javac工具编译项目源码,生成Class文件。
  4. 通过DX工具将所有的Class文件转换成DEX文件,该过程主要完成Java字节码转换成Dalvik字节码,压缩常量池以及清除冗余信息等工作。
  5. 通过ApkBuilder工具将资源文件、DEX文件打包生成APK文件。
  6. 利用KeyStore对生成的APK文件进行签名。
  7. 如果是正式版的APK,还会利用ZipAlign工具进行对齐处理,对齐的过程就是将APK文件中所有的资源文件举例文件的起始距离都偏移4字节的整数倍,这样通过内存映射访问APK文件
    的速度会更快。

了解APK的安装流程吗,描述一下?

APK的安装流程如下所示:
image

  1. 复制APK到/data/app目录下,解压并扫描安装包。
  2. 资源管理器解析APK里的资源文件。
  3. 解析AndroidManifest文件,并在/data/data/目录下创建对应的应用数据目录。
  4. 然后对dex文件进行优化,并保存在dalvik-cache目录下。
  5. 将AndroidManifest文件解析出的四大组件信息注册到PackageManagerService中。
  6. 安装完成后,发送广播。

当点击一个应用图标以后,都发生了什么,描述一下这个过程?

点击应用图标后会去启动应用的LauncherActivity,如果LancerActivity所在的进程没有创建,还会创建新进程,整体的流程就是一个Activity的启动流程。

Activity的启动流程图(放大可查看)如下所示:
image

整个流程涉及的主要角色有:

  • Instrumentation: 监控应用与系统相关的交互行为。
  • AMS:组件管理调度中心,什么都不干,但是什么都管。
  • ActivityStarter:Activity启动的控制器,处理Intent与Flag对Activity启动的影响,具体说来有:1 寻找符合启动条件的Activity,如果有多个,让用户选择;2 校验启动参数的合法性;3 返回int参数,代表Activity是否启动成功。
  • ActivityStackSupervisior:这个类的作用你从它的名字就可以看出来,它用来管理任务栈。
  • ActivityStack:用来管理任务栈里的Activity。
  • ActivityThread:最终干活的人,是ActivityThread的内部类,Activity、Service、BroadcastReceiver的启动、切换、调度等各种操作都在这个类里完成。

注:这里单独提一下ActivityStackSupervisior,这是高版本才有的类,它用来管理多个ActivityStack,早期的版本只有一个ActivityStack对应着手机屏幕,后来高版本支持多屏以后,就
有了多个ActivityStack,于是就引入了ActivityStackSupervisior用来管理多个ActivityStack。

整个流程主要涉及四个进程:

  • 调用者进程,如果是在桌面启动应用就是Launcher应用进程。
  • ActivityManagerService等所在的System Server进程,该进程主要运行着系统服务组件。
  • Zygote进程,该进程主要用来fork新进程。
  • 新启动的应用进程,该进程就是用来承载应用运行的进程了,它也是应用的主线程(新创建的进程就是主线程),处理组件生命周期、界面绘制等相关事情。

有了以上的理解,整个流程可以概括如下:

  1. 点击桌面应用图标,Launcher进程将启动Activity(MainActivity)的请求以Binder的方式发送给了AMS。
  2. AMS接收到启动请求后,交付ActivityStarter处理Intent和Flag等信息,然后再交给ActivityStackSupervisior/ActivityStack
    处理Activity进栈相关流程。同时以Socket方式请求Zygote进程fork新进程。
  3. Zygote接收到新进程创建请求后fork出新进程。
  4. 在新进程里创建ActivityThread对象,新创建的进程就是应用的主线程,在主线程里开启Looper消息循环,开始处理创建Activity。
  5. ActivityThread利用ClassLoader去加载Activity、创建Activity实例,并回调Activity的onCreate()方法。这样便完成了Activity的启动。

BroadcastReceiver与LocalBroadcastReceiver有什么区别?

  • BroadcastReceiver 是跨应用广播,利用Binder机制实现。
  • LocalBroadcastReceiver 是应用内广播,利用Handler实现,利用了IntentFilter的match功能,提供消息的发布与接收功能,实现应用内通信,效率比较高。

Android Handler机制是做什么的,原理了解吗?

Android消息循环流程图如下所示:
image

主要涉及的角色如下所示:

  • Message:消息,分为硬件产生的消息(例如:按钮、触摸)和软件产生的消息。
  • MessageQueue:消息队列,主要用来向消息池添加消息和取走消息。
  • Looper:消息循环器,主要用来把消息分发给相应的处理者。
  • Handler:消息处理器,主要向消息队列发送各种消息以及处理各种消息。

整个消息的循环流程还是比较清晰的,具体说来:

  1. Handler通过sendMessage()发送消息Message到消息队列MessageQueue。
  2. Looper通过loop()不断提取触发条件的Message,并将Message交给对应的target handler来处理。
  3. target handler调用自身的handleMessage()方法来处理Message。

事实上,在整个消息循环的流程中,并不只有Java层参与,很多重要的工作都是在C++层来完成的。我们来看下这些类的调用关系。
image

注:虚线表示关联关系,实线表示调用关系。

在这些类中MessageQueue是Java层与C++层维系的桥梁,MessageQueue与Looper相关功能都通过MessageQueue的Native方法来完成,而其他虚线连接的类只有关联关系,并没有
直接调用的关系,它们发生关联的桥梁是MessageQueue。

Android Binder机制是做什么的,为什么选用Binder,原理了解吗?

Android Binder是用来做进程通信的,Android的各个应用以及系统服务都运行在独立的进程中,它们的通信都依赖于Binder。

为什么选用Binder,在讨论这个问题之前,我们知道Android也是基于Linux内核,Linux现有的进程通信手段有以下几种:

  1. 管道:在创建时分配一个page大小的内存,缓存区大小比较有限;
  2. 消息队列:信息复制两次,额外的CPU消耗;不合适频繁或信息量大的通信;
  3. 共享内存:无须复制,共享缓冲区直接付附加到进程虚拟地址空间,速度快;但进程间的同步问题操作系统无法实现,必须各进程利用同步工具解决;
  4. 套接字:作为更通用的接口,传输效率低,主要用于不通机器或跨网络的通信;
  5. 信号量:常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。6. 信号: 不适用于信息交换,更适用于进程中断控制,比如非法内存访问,杀死某个进程等;

既然有现有的IPC方式,为什么重新设计一套Binder机制呢。主要是出于以上三个方面的考量:

  • 高性能:从数据拷贝次数来看Binder只需要进行一次内存拷贝,而管道、消息队列、Socket都需要两次,共享内存不需要拷贝,Binder的性能仅次于共享内存。
  • 稳定性:上面说到共享内存的性能优于Binder,那为什么不适用共享内存呢,因为共享内存需要处理并发同步问题,控制负责,容易出现死锁和资源竞争,稳定性较差。而Binder基于C/S架构,客户端与服务端彼此独立,稳定性较好。
  • 安全性:我们知道Android为每个应用分配了UID,用来作为鉴别进程的重要标志,Android内部也依赖这个UID进行权限管理,包括6.0以前的固定权限和6.0以后的动态权限,传荣IPC只能由用户在数据包里填入UID/PID,这个标记完全
    是在用户空间控制的,没有放在内核空间,因此有被恶意篡改的可能,因此Binder的安全性更高。

描述一下Activity的生命周期,这些生命周期是如何管理的?

Activity与Fragment生命周期如下所示:
image

读者可以从上图看出,Activity有很多种状态,状态之间的变化也比较复杂,在众多状态中,只有三种是常驻状态:

  • Resumed(运行状态):Activity处于前台,用户可以与其交互。
  • Paused(暂停状态):Activity被其他Activity部分遮挡,无法接受用户的输入。
  • Stopped(停止状态):Activity被完全隐藏,对用户不可见,进入后台。

其他的状态都是中间状态。

我们再来看看生命周期变化时的整个调度流程,生命周期调度流程图如下所示:
image

所以你可以看到,整个流程是这样的:

  1. 比方说我们点击跳转一个新Activity,这个时候Activity会入栈,同时它的生命周期也会从onCreate()到onResume()开始变换,这个过程是在ActivityStack里完成的,ActivityStack
    是运行在Server进程里的,这个时候Server进程就通过ApplicationThread的代理对象ApplicationThreadProxy向运行在app进程ApplicationThread发起操作请求。
  2. ApplicationThread接收到操作请求后,因为它是运行在app进程里的其他线程里,所以ApplicationThread需要通过Handler向主线程ActivityThread发送操作消息。
  3. 主线程接收到ApplicationThread发出的消息后,调用主线程ActivityThread执行响应的操作,并回调Activity相应的周期方法。

注:这里提到了主线程ActivityThread,更准确来说ActivityThread不是线程,因为它没有继承Thread类或者实现Runnable接口,它是运行在应用主线程里的对象,那么应用的主线程
到底是什么呢?从本质上来讲启动启动时创建的进程就是主线程,线程和进程处理是否共享资源外,没有其他的区别,对于Linux来说,它们都只是一个struct结构体。

Activity的通信方式有哪些?

  • startActivityForResult
  • EventBus
  • LocalBroadcastReceiver

Android应用里有几种Context对象,

Context类图如下所示:
image

可以发现Context是个抽象类,它的具体实现类是ContextImpl,ContextWrapper是个包装类,内部的成员变量mBase指向的也是个ContextImpl对象,ContextImpl完成了
实际的功能,Activity、Service与Application都直接或者间接的继承ContextWrapper。

描述一下进程和Application的生命周期?

一个安装的应用对应一个LoadedApk对象,对应一个Application对象,对于四大组件,Application的创建和获取方式也是不尽相同的,具体说来:

  • Activity:通过LoadedApk的makeApplication()方法创建。
  • Service:通过LoadedApk的makeApplication()方法创建。
  • 静态广播:通过其回调方法onReceive()方法的第一个参数指向Application。
  • ContentProvider:无法获取Application,因此此时Application不一定已经初始化。

Android哪些情况会导致内存泄漏,如何分析内存泄漏?

常见的产生内存泄漏的情况如下所示:

  • 持有静态的Context(Activity)引用。
  • 持有静态的View引用,
  • 内部类&匿名内部类实例无法释放(有延迟时间等等),而内部类又持有外部类的强引用,导致外部类无法释放,这种匿名内部类常见于监听器、Handler、Thread、TimerTask
  • 资源使用完成后没有关闭,例如:BraodcastReceiver,ContentObserver,File,Cursor,Stream,Bitmap。
  • 不正确的单例模式,比如单例持有Activity。
  • 集合类内存泄漏,如果一个集合类是静态的(缓存HashMap),只有添加方法,没有对应的删除方法,会导致引用无法被释放,引发内存泄漏。
  • 错误的覆写了finalize()方法,finalize()方法执行执行不确定,可能会导致引用无法被释放。

查找内存泄漏可以使用Android Profiler工具或者利用LeakCanary工具。

Android有哪几种进程,是如何管理的?

Android的进程主要分为以下几种:

前台进程

用户当前操作所必需的进程。如果一个进程满足以下任一条件,即视为前台进程:

  • 托管用户正在交互的 Activity(已调用 Activity 的 onResume() 方法)
  • 托管某个 Service,后者绑定到用户正在交互的 Activity
  • 托管正在“前台”运行的 Service(服务已调用 startForeground())
  • 托管正执行一个生命周期回调的 Service(onCreate()、onStart() 或 onDestroy())
  • 托管正执行其 onReceive() 方法的 BroadcastReceiver

通常,在任意给定时间前台进程都为数不多。只有在内存不足以支持它们同时继续运行这一万不得已的情况下,系统才会终止它们。 此时,设备往往已达到内存分页状态,因此需要终止一些前台进程来确保用户界面正常响应。

可见进程

没有任何前台组件、但仍会影响用户在屏幕上所见内容的进程。 如果一个进程满足以下任一条件,即视为可见进程:

  • 托管不在前台、但仍对用户可见的 Activity(已调用其 onPause() 方法)。例如,如果前台 Activity 启动了一个对话框,允许在其后显示上一 Activity,则有可能会发生这种情况。
  • 托管绑定到可见(或前台)Activity 的 Service。

可见进程被视为是极其重要的进程,除非为了维持所有前台进程同时运行而必须终止,否则系统不会终止这些进程。

服务进程

正在运行已使用 startService() 方法启动的服务且不属于上述两个更高类别进程的进程。尽管服务进程与用户所见内容没有直接关联,但是它们通常在执行一些用户关
心的操作(例如,在后台播放音乐或从网络下载数据)。因此,除非内存不足以维持所有前台进程和可见进程同时运行,否则系统会让服务进程保持运行状态。

后台进程

包含目前对用户不可见的 Activity 的进程(已调用 Activity 的 onStop() 方法)。这些进程对用户体验没有直接影响,系统可能随时终止它们,以回收内存供前台进程、可见进程或服务进程使用。 通常会有很多后台进程在运行,因此它们会保存在 LRU (最近最少使用)列表中,以确保包含用户最近查看的 Activity 的进程最后一个被终止。如果某个 Activity 正确实现了生命周期方法,并保存了其当前状态,则终止其进程不会对用户体验产生明显影响,因为当用户导航回该 Activity 时,Activity 会恢复其所有可见状态。

空进程

不含任何活动应用组件的进程。保留这种进程的的唯一目的是用作缓存,以缩短下次在其中运行组件所需的启动时间。 为使总体系统资源在进程缓存和底层内核缓存之间保持平衡,系统往往会终止这些进程。

ActivityManagerService负责根据各种策略算法计算进程的adj值,然后交由系统内核进行进程的管理。

SharePreference性能优化,可以做进程同步吗?

在Android中, SharePreferences是一个轻量级的存储类,特别适合用于保存软件配置参数。使用SharedPreferences保存数据,其背后是用xml文件存放数据,文件
存放在/data/data/ < package name > /shared_prefs目录下.

之所以说SharedPreference是一种轻量级的存储方式,是因为它在创建的时候会把整个文件全部加载进内存,如果SharedPreference文件比较大,会带来以下问题:

  1. 第一次从sp中获取值的时候,有可能阻塞主线程,使界面卡顿、掉帧。
  2. 解析sp的时候会产生大量的临时对象,导致频繁GC,引起界面卡顿。
  3. 这些key和value会永远存在于内存之中,占用大量内存。

优化建议

  1. 不要存放大的key和value,会引起界面卡、频繁GC、占用内存等等。
  2. 毫不相关的配置项就不要放在在一起,文件越大读取越慢。
  3. 读取频繁的key和不易变动的key尽量不要放在一起,影响速度,如果整个文件很小,那么忽略吧,为了这点性能添加维护成本得不偿失。
  4. 不要乱edit和apply,尽量批量修改一次提交,多次apply会阻塞主线程。
  5. 尽量不要存放JSON和HTML,这种场景请直接使用JSON。
  6. SharedPreference无法进行跨进程通信,MODE_MULTI_PROCESS只是保证了在API 11以前的系统上,如果sp已经被读取进内存,再次获取这个SharedPreference的时候,如果有这个flag,会重新读一遍文件,仅此而已。

如何做SQLite升级?

数据库升级增加表和删除表都不涉及数据迁移,但是修改表涉及到对原有数据进行迁移。升级的方法如下所示:

  1. 将现有表命名为临时表。
  2. 创建新表。
  3. 将临时表的数据导入新表。
  4. 删除临时表。

重写

如果是跨版本数据库升级,可以由两种方式,如下所示:

  1. 逐级升级,确定相邻版本与现在版本的差别,V1升级到V2,V2升级到V3,依次类推。
  2. 跨级升级,确定每个版本与现在数据库的差别,为每个case编写专门升级大代码。

进程保护如何做,如何唤醒其他进程?

进程保活主要有两个思路:

  1. 提升进程的优先级,降低进程被杀死的概率。
  2. 拉活已经被杀死的进程。

如何提升优先级,如下所示:

监控手机锁屏事件,在屏幕锁屏时启动一个像素的Activity,在用户解锁时将Activity销毁掉,前台Activity可以将进程变成前台进程,优先级升级到最高。

如果拉活

利用广播拉活Activity。

理解序列化吗,Android为什么引入Parcelable?

所谓序列化就是将对象变成二进制流,便于存储和传输。

  • Serializable是java实现的一套序列化方式,可能会触发频繁的IO操作,效率比较低,适合将对象存储到磁盘上的情况。
  • Parcelable是Android提供一套序列化机制,它将序列化后的字节流写入到一个共性内存中,其他对象可以从这块共享内存中读出字节流,并反序列化成对象。因此效率比较高,适合在对象间或者进程间传递信息。

如何计算一个Bitmap占用内存的大小,怎么保证加载Bitmap不产生内存溢出?

Bitamp 占用内存大小 = 宽度像素 x (inTargetDensity / inDensity) x 高度像素 x (inTargetDensity / inDensity)x 一个像素所占的内存

注:这里inDensity表示目标图片的dpi(放在哪个资源文件夹下),inTargetDensity表示目标屏幕的dpi,所以你可以发现inDensity和inTargetDensity会对Bitmap的宽高
进行拉伸,进而改变Bitmap占用内存的大小。

在Bitmap里有两个获取内存占用大小的方法。

  • getByteCount():API12 加入,代表存储 Bitmap 的像素需要的最少内存。
  • getAllocationByteCount():API19 加入,代表在内存中为 Bitmap 分配的内存大小,代替了 getByteCount() 方法。

在不复用 Bitmap 时,getByteCount() 和 getAllocationByteCount 返回的结果是一样的。在通过复用 Bitmap 来解码图片时,那么 getByteCount() 表示新解码图片占用内存的大
小,getAllocationByteCount() 表示被复用 Bitmap真实占用的内存大小(即 mBuffer 的长度)。

为了保证在加载Bitmap的时候不产生内存溢出,可以受用BitmapFactory进行图片压缩,主要有以下几个参数:

  • BitmapFactory.Options.inPreferredConfig:将ARGB_8888改为RGB_565,改变编码方式,节约内存。
  • BitmapFactory.Options.inSampleSize:缩放比例,可以参考Luban那个库,根据图片宽高计算出合适的缩放比例。
  • BitmapFactory.Options.inPurgeable:让系统可以内存不足时回收内存。

Android如何在不压缩的情况下加载高清大图?

使用BitmapRegionDecoder进行布局加载。

Android里的内存缓存和磁盘缓存是怎么实现的。

内存缓存基于LruCache实现,磁盘缓存基于DiskLruCache实现。这两个类都基于Lru算法和LinkedHashMap来实现。

LRU算法可以用一句话来描述,如下所示:

LRU是Least Recently Used的缩写,最近最久未使用算法,从它的名字就可以看出,它的核心原则是如果一个数据在最近一段时间没有使用到,那么它在将来被
访问到的可能性也很小,则这类数据项会被优先淘汰掉。

LruCache的原理是利用LinkedHashMap持有对象的强引用,按照Lru算法进行对象淘汰。具体说来假设我们从表尾访问数据,在表头删除数据,当访问的数据项在链表中存在时,则将该数据项移动到表尾,否则在表尾新建一个数据项。当链表容量超过一定阈值,则移除表头的数据。

为什么会选择LinkedHashMap呢?

这跟LinkedHashMap的特性有关,LinkedHashMap的构造函数里有个布尔参数accessOrder,当它为true时,LinkedHashMap会以访问顺序为序排列元素,否则以插入顺序为序排序元素。

DiskLruCache与LruCache原理相似,只是多了一个journal文件来做磁盘文件的管理和迎神,如下所示:

1
2
3
4
5
6
7
8
libcore.io.DiskLruCache
1
1
1

DIRTY 1517126350519
CLEAN 1517126350519 5325928
REMOVE 1517126350519

注:这里的缓存目录是应用的缓存目录/data/data/pckagename/cache,未root的手机可以通过以下命令进入到该目录中或者将该目录整体拷贝出来:

1
2
3
4
5
6
7
8

//进入/data/data/pckagename/cache目录
adb shell
run-as com.your.packagename
cp /data/data/com.your.packagename/

//将/data/data/pckagename目录拷贝出来
adb backup -noapk com.your.packagename

我们来分析下这个文件的内容:

  • 第一行:libcore.io.DiskLruCache,固定字符串。
  • 第二行:1,DiskLruCache源码版本号。
  • 第三行:1,App的版本号,通过open()方法传入进去的。
  • 第四行:1,每个key对应几个文件,一般为1.
  • 第五行:空行
  • 第六行及后续行:缓存操作记录。

第六行及后续行表示缓存操作记录,关于操作记录,我们需要了解以下三点:

  1. DIRTY 表示一个entry正在被写入。写入分两种情况,如果成功会紧接着写入一行CLEAN的记录;如果失败,会增加一行REMOVE记录。注意单独只有DIRTY状态的记录是非法的。
  2. 当手动调用remove(key)方法的时候也会写入一条REMOVE记录。
  3. READ就是说明有一次读取的记录。
  4. CLEAN的后面还记录了文件的长度,注意可能会一个key对应多个文件,那么就会有多个数字。

PathClassLoader与DexClassLoader有什么区别?

  • PathClassLoader:只能加载已经安装到Android系统的APK文件,即/data/app目录,Android默认的类加载器。
  • DexClassLoader:可以加载任意目录下的dex、jar、apk、zip文件。

WebView优化了解吗,如何提高WebView的加载速度?

为什么WebView加载会慢呢?

这是因为在客户端中,加载H5页面之前,需要先初始化WebView,在WebView完全初始化完成之前,后续的界面加载过程都是被阻塞的。

优化手段围绕着以下两个点进行:

  1. 预加载WebView。
  2. 加载WebView的同时,请求H5页面数据。

因此常见的方法是:

  1. 全局WebView。
  2. 客户端代理页面请求。WebView初始化完成后向客户端请求数据。
  3. asset存放离线包。

除此之外还有一些其他的优化手段:

  • 脚本执行慢,可以让脚本最后运行,不阻塞页面解析。
  • DNS与链接慢,可以让客户端复用使用的域名与链接。
  • React框架代码执行慢,可以将这部分代码拆分出来,提前进行解析。

Java和JS的相互调用怎么实现,有做过什么优化吗?

jockeyjs:https://github.com/tcoulter/jockeyjs

对协议进行统一的封装和处理。

JNI了解吗,Java与C++如何相互调用?

Java调用C++

  1. 在Java中声明Native方法(即需要调用的本地方法)
  2. 编译上述 Java源文件javac(得到 .class文件)
    3。 通过 javah 命令导出JNI的头文件(.h文件)
  3. 使用 Java需要交互的本地代码 实现在 Java中声明的Native方法
  4. 编译.so库文件
  5. 通过Java命令执行 Java程序,最终实现Java调用本地代码

C++调用Java

  1. 从classpath路径下搜索ClassMethod这个类,并返回该类的Class对象。
  2. 获取类的默认构造方法ID。
  3. 查找实例方法的ID。
  4. 创建该类的实例。
  5. 调用对象的实例方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
JNIEXPORT void JNICALL Java_com_study_jnilearn_AccessMethod_callJavaInstaceMethod  
(JNIEnv *env, jclass cls)
{
jclass clazz = NULL;
jobject jobj = NULL;
jmethodID mid_construct = NULL;
jmethodID mid_instance = NULL;
jstring str_arg = NULL;
// 1、从classpath路径下搜索ClassMethod这个类,并返回该类的Class对象
clazz = (*env)->FindClass(env, "com/study/jnilearn/ClassMethod");
if (clazz == NULL) {
printf("找不到'com.study.jnilearn.ClassMethod'这个类");
return;
}

// 2、获取类的默认构造方法ID
mid_construct = (*env)->GetMethodID(env,clazz, "<init>","()V");
if (mid_construct == NULL) {
printf("找不到默认的构造方法");
return;
}

// 3、查找实例方法的ID
mid_instance = (*env)->GetMethodID(env, clazz, "callInstanceMethod", "(Ljava/lang/String;I)V");
if (mid_instance == NULL) {

return;
}

// 4、创建该类的实例
jobj = (*env)->NewObject(env,clazz,mid_construct);
if (jobj == NULL) {
printf("在com.study.jnilearn.ClassMethod类中找不到callInstanceMethod方法");
return;
}

// 5、调用对象的实例方法
str_arg = (*env)->NewStringUTF(env,"我是实例方法");
(*env)->CallVoidMethod(env,jobj,mid_instance,str_arg,200);

// 删除局部引用
(*env)->DeleteLocalRef(env,clazz);
(*env)->DeleteLocalRef(env,jobj);
(*env)->DeleteLocalRef(env,str_arg);
}

了解插件化和热修复吗,它们有什么区别,理解它们的原理吗?

  • 插件化:插件化是体现在功能拆分方面的,它将某个功能独立提取出来,独立开发,独立测试,再插入到主应用中。依次来较少主应用的规模。
  • 热修复:热修复是体现在bug修复方面的,它实现的是不需要重新发版和重新安装,就可以去修复已知的bug。

利用PathClassLoader和DexClassLoader去加载与bug类同名的类,替换掉bug类,进而达到修复bug的目的,原理是在app打包的时候阻止类打上CLASS_ISPREVERIFIED标志,然后在
热修复的时候动态改变BaseDexClassLoader对象间接引用的dexElements,替换掉旧的类。

目前热修复框架主要分为两大类:

  • Sophix:修改方法指针。
  • Tinker:修改dex数组元素。

如何做性能优化?

  1. 节制的使用Service,当启动一个Service时,系统总是倾向于保留这个Service依赖的进程,这样会造成系统资源的浪费,可以使用IntentService,执行完成任务后会自动停止。
  2. 当界面不可见时释放内存,可以重写Activity的onTrimMemory()方法,然后监听TRIM_MEMORY_UI_HIDDEN这个级别,这个级别说明用户离开了页面,可以考虑释放内存和资源。
  3. 避免在Bitmap浪费过多的内存,使用压缩过的图片,也可以使用Fresco等库来优化对Bitmap显示的管理。
  4. 使用优化过的数据集合SparseArray代替HashMap,HashMap为每个键值都提供一个对象入口,使用SparseArray可以免去基本对象类型转换为引用数据类想的时间。

如果防止过度绘制,如何做布局优化?

  1. 使用include复用布局文件。
  2. 使用merge标签避免嵌套布局。
  3. 使用stub标签仅在需要的时候在展示出来。

如何提交代码质量?

  1. 避免创建不必要的对象,尽可能避免频繁的创建临时对象,例如在for循环内,减少GC的次数。
  2. 尽量使用基本数据类型代替引用数据类型。
  3. 静态方法调用效率高于动态方法,也可以避免创建额外对象。
  4. 对于基本数据类型和String类型的常量要使用static final修饰,这样常量会在dex文件的初始化器中进行初始化,使用的时候可以直接使用。
  5. 多使用系统API,例如数组拷贝System.arrayCopy()方法,要比我们用for循环效率快9倍以上,因为系统API很多都是通过底层的汇编模式执行的,效率比较高。

有没有遇到64k问题,为什么,如何解决?

  • 在DEX文件中,method、field、class等的个数使用short类型来做索引,即两个字节(65535),method、field、class等均有此限制。
  • APK在安装过程中会调用dexopt将DEX文件优化成ODEX文件,dexopt使用LinearAlloc来存储应用信息,关于LinearAlloc缓冲区大小,不同的版本经历了4M/8M/16M的限制,超出
    缓冲区时就会抛出INSTALL_FAILED_DEXOPT错误。

解决方案是Google的MultiDex方案,具体参见:配置方法数超过 64K 的应用

MVC、MVP与MVVM之间的对比分析?

  • MVC:PC时代就有的架构方案,在Android上也是最早的方案,Activity/Fragment这些上帝角色既承担了V的角色,也承担了C的角色,小项目开发起来十分顺手,大项目就会遇到
    耦合过重,Activity/Fragment类过大等问题。
  • MVP:为了解决MVC耦合过重的问题,MVP的核心思想就是提供一个Presenter将视图逻辑I和业务逻辑相分离,达到解耦的目的。
  • MVVM:使用ViewModel代替Presenter,实现数据与View的双向绑定,这套框架最早使用的data-binding将数据绑定到xml里,这么做在大规模应用的时候是不行的,不过数据绑定是
    一个很有用的概念,后续Google又推出了ViewModel组件与LiveData组件。ViewModel组件规范了ViewModel所处的地位、生命周期、生产方式以及一个Activity下多个Fragment共享View
    Model数据的问题。LiveData组件则提供了在Java层面View订阅ViewModel数据源的实现方案。

网络编程

TCP与UDP有什么区别?

  1. TCP面向连接(如打电话要先拨号建立连接);UDP是无连接的,即发送数据之前不需要建立连接
  2. TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付
  3. TCP面向字节流,实际上是TCP把数据看成一连串无结构的字节流;UDP是面向报文的UDP没有拥塞控制,因此网络出现拥塞不会使源主机的发送速率降低(对实时应用很有用,如IP电话,实时视频会议等)
  4. 每一条TCP连接只能是点到点的;UDP支持一对一,一对多,多对一和多对多的交互通信
  5. TCP首部开销20字节;UDP的首部开销小,只有8个字节
  6. TCP的逻辑通信信道是全双工的可靠信道,UDP则是不可靠信道

简单介绍一下TCP三次握手与四次分手过程?

TCP用三次握手(three-way handshake)过程创建一个连接,使用四次分手
关闭一个连接。

三次握手与四次分手的流程如下所示:

三次握手

  • 第一次握手:建立连接。客户端发送连接请求报文段,将SYN位置为1,Sequence Number为x;然后,客户端进入SYN_SEND状态,等待服务器的确认;
  • 第二次握手:服务器收到SYN报文段。服务器收到客户端的SYN报文段,需要对这个SYN报文段进行确认,设置Acknowledgment Number为x+1(Sequence Number+1);同时,自己自己还要发送SYN请求信息,将SYN位置为1,Sequence Number为y;服务器端将上述所有信息放到一个报文段(即SYN+ACK报文段)中,一并发送给客户端,此时服务器进入SYN_RECV状态;
  • 第三次握手:客户端收到服务器的SYN+ACK报文段。然后将Acknowledgment Number设置为y+1,向服务器发送ACK报文段,这个报文段发送完毕以后,客户端和服务器端都进入ESTABLISHED状态,完成TCP三次握手。
    完成了三次握手,客户端和服务器端就可以开始传送数据。以上就是TCP三次握手的总体介绍。

四次分手

  • 第一次分手:主机1(可以使客户端,也可以是服务器端),设置Sequence Number和Acknowledgment Number,向主机2发送一个FIN报文段;此时,主机1进入FIN_WAIT_1状态;这表示主机1没有数据要发送给主机2了;
  • 第二次分手:主机2收到了主机1发送的FIN报文段,向主机1回一个ACK报文段,Acknowledgment Number为Sequence Number加1;主机1进入FIN_WAIT_2状态;主机2告诉主机1,我“同意”你的关闭请求;
  • 第三次分手:主机2向主机1发送FIN报文段,请求关闭连接,同时主机2进入LAST_ACK状态;
  • 第四次分手:主机1收到主机2发送的FIN报文段,向主机2发送ACK报文段,然后主机1进入TIME_WAIT状态;主机2收到主机1的ACK报文段以后,就关闭连接;此时,主机1等待2MSL后依然没有收到回复,则证明Server端已正常关闭,那好,主机1也可以关闭连接了。

三次握手与四次分手也是个老生常谈的概念,举个简单的例子说明一下。

三次握手

例如你小时候出去玩,经常玩忘了回家吃饭。你妈妈也经常过来喊你。如果你没有走远,在门口的小土堆上玩泥巴,你妈妈会喊:”小新,回家吃饭了”。你听到后会回应:”知道了,一会就回去”。妈妈听
到你的回应后又说:”快点回来,饭要凉了”。这样你妈妈和你就完成了三次握手的过程。😁说到这里你也可以理解三次握手的必要性,少了其中一个环节,另一方就会陷入等待之中。

三次握手的目的是为了防止已失效的连接请求报文段突然又传送到了服务端,因而产生错误.

四次分手

例如偶像言情剧干净利落的分手,女主对男主说:我们分手吧🙄,男主说:分就分吧😰。女主说:你果然是不爱我了,你只知道让我多喝热水🙄。男主说:事到如今也没什么好说的了,祝你幸福🙃。四次分手完成。说到这里你可以理解
了四次分手的必要性,第一次是女方(客户端)提出分手,第二次是男主(服务端)同意女主分手,第三次是女主确定男主不再爱她,也同意男主分手。第四次两人彻底拜拜(断开连接)。

因为TCP是全双工模式,所以四次分手的目的就是为了可靠地关闭连接。

TCP如何保证数据传输的可靠性?

  1. 确认和重传:接收方收到报文后就会进行确认,发送方一段时间没有收到确认就会重传。
  2. 数据校验。
  3. 数据合理分片与排序,TCP会对数据进行分片,接收方会缓存为按序到达的数据,重新排序后再提交给应用层。
  4. 流程控制:当接收方来不及接收发送的数据时,则会提示发送方降低发送的速度,防止包丢失。发送者的发送速度与接收者的接收能力相关。接收者会把它能接收的最大字节数(未使用的缓冲区大小,又叫接收窗口,receive window)告知发送者。发送者发送的最大字节数与接收者的接收窗口大小一致。
  5. 拥塞控制:当网络发生拥塞时,减少数据的发送。
    阻塞窗口是不同于接收窗口的另一个概念,它通过限制网络中的数据流的体积来防止网络阻塞。类似于接收窗口,发送者通过通过一些算法(例如TCP Vegas,Westwood,BIC,CUBIC)来计算发送对应的接收者的阻塞窗口能容纳的最多的数据。和流量控制不同,阻塞控制只在发送方实现。(译注:发送者类似于通过ack时间之类的算法判断当前网络是否阻塞,从而调节发送速度

    HTTP与HTTPS有什么区别?

HTTPS是一种通过计算机网络进行安全通信的传输协议。HTTPS经由HTTP进行通信,但利用SSL/TLS来加密数据包。HTTPS开发的主要目的,是提供对网站服务器的身份
认证,保护交换数据的隐私与完整性。

如下图所示,可以很明显的看出两个的区别:
image

注:TLS是SSL的升级替代版,具体发展历史可以参考传输层安全性协议

HTTP与HTTPS在写法上的区别也是前缀的不同,客户端处理的方式也不同,具体说来:

  • 如果URL的协议是HTTP,则客户端会打开一条到服务端端口80(默认)的连接,并向其发送老的HTTP请求。
  • 如果URL的协议是HTTPS,则客户端会打开一条到服务端端口443(默认)的连接,然后与服务器握手,以二进制格式与服务器交换一些SSL的安全参数,附上加密的
    HTTP请求。

所以你可以看到,HTTPS比HTTP多了一层与SSL的连接,这也就是客户端与服务端SSL握手的过程,整个过程主要完成以下工作:

  • 交换协议版本号
  • 选择一个两端都了解的密码
  • 对两端的身份进行认证
  • 生成临时的会话密钥,以便加密信道。

SSL握手是一个相对比较复杂的过程,更多关于SSL握手的过程细节可以参考TLS/SSL握手过程

SSL/TSL的常见开源实现是OpenSSL,OpenSSL是一个开放源代码的软件库包,应用程序可以使用这个包来进行安全通信,避免窃听,同时确认另一端连接者的身份。这个包广泛被应用在互联网的网页服务器上。
更多源于OpenSSL的技术细节可以参考OpenSSL

caflow:

https flow

认证服务器。浏览器内置一个受信任的CA机构列表,并保存了这些CA机构的证书。第一阶段服务器会提供经CA机构认证颁发的服务器证书,如果认证该服务器证书的CA机构,存在于浏览器的受信任CA机构列表中,并且服务器证书中的信息与当前正在访问的网站(域名等)一致,那么浏览器就认为服务端是可信的,并从服务器证书中取得服务器公钥,用于后续流程。否则,浏览器将提示用户,根据用户的选择,决定是否继续。当然,我们可以管理这个受信任CA机构列表,添加我们想要信任的CA机构,或者移除我们不信任的CA机构。

协商会话密钥。客户端在认证完服务器,获得服务器的公钥之后,利用该公钥与服务器进行加密通信,协商出两个会话密钥,分别是用于加密客户端往服务端发送数据的客户端会话密钥,用于加密服务端往客户端发送数据的服务端会话密钥。在已有服务器公钥,可以加密通讯的前提下,还要协商两个对称密钥的原因,是因为非对称加密相对复杂度更高,在数据传输过程中,使用对称加密,可以节省计算资源。另外,会话密钥是随机生成,每次协商都会有不一样的结果,所以安全性也比较高。

加密通讯。此时客户端服务器双方都有了本次通讯的会话密钥,之后传输的所有Http数据,都通过会话密钥加密。这样网路上的其它用户,将很难窃取和篡改客户端和服务端之间传输的数据,从而保证了数据的私密性和完整性。

谈一谈对HTTP缓存的理解?

HTTP的缓存机制也是依赖于请求和响应header里的参数类实现的,最终响应式从缓存中去,还是从服务端重新拉取,HTTP的缓存机制的流程如下所示:

HTTP的缓存可以分为两种:

  • 强制缓存:需要服务端参与判断是否继续使用缓存,当客户端第一次请求数据是,服务端返回了缓存的过期时间(Expires与Cache-Control),没有过期就可以继续使用缓存,否则则不适用,无需再向服务端询问。
  • 对比缓存:需要服务端参与判断是否继续使用缓存,当客户端第一次请求数据时,服务端会将缓存标识(Last-Modified/If-Modified-Since与Etag/If-None-Match)与数据一起返回给客户端,客户端将两者都备份到缓存中 ,再次请求数据时,客户端将上次备份的缓存
    标识发送给服务端,服务端根据缓存标识进行判断,如果返回304,则表示通知客户端可以继续使用缓存。

强制缓存优先于对比缓存。

上面提到强制缓存使用的的两个标识:

  • Expires:Expires的值为服务端返回的到期时间,即下一次请求时,请求时间小于服务端返回的到期时间,直接使用缓存数据。到期时间是服务端生成的,客户端和服务端的时间可能有误差。
  • Cache-Control:Expires有个时间校验的问题,所有HTTP1.1采用Cache-Control替代Expires。

Cache-Control的取值有以下几种:

  • private: 客户端可以缓存。
  • public: 客户端和代理服务器都可缓存。
  • max-age=xxx: 缓存的内容将在 xxx 秒后失效
  • no-cache: 需要使用对比缓存来验证缓存数据。
  • no-store: 所有内容都不会缓存,强制缓存,对比缓存都不会触发。

我们再来看看对比缓存的两个标识:

Last-Modified/If-Modified-Since

Last-Modified 表示资源上次修改的时间。

当客户端发送第一次请求时,服务端返回资源上次修改的时间:

1
Last-Modified: Tue, 12 Jan 2016 09:31:27 GMT

客户端再次发送,会在header里携带If-Modified-Since。将上次服务端返回的资源时间上传给服务端。

1
If-Modified-Since: Tue, 12 Jan 2016 09:31:27 GMT

服务端接收到客户端发来的资源修改时间,与自己当前的资源修改时间进行对比,如果自己的资源修改时间大于客户端发来的资源修改时间,则说明资源做过修改,
则返回200表示需要重新请求资源,否则返回304表示资源没有被修改,可以继续使用缓存。

上面是一种时间戳标记资源是否修改的方法,还有一种资源标识码ETag的方式来标记是否修改,如果标识码发生改变,则说明资源已经被修改,ETag优先级高于Last-Modified。

Etag/If-None-Match

ETag是资源文件的一种标识码,当客户端发送第一次请求时,服务端会返回当前资源的标识码:

1
ETag: "5694c7ef-24dc"

客户端再次发送,会在header里携带上次服务端返回的资源标识码:

1
If-None-Match:"5694c7ef-24dc"

服务端接收到客户端发来的资源标识码,则会与自己当前的资源吗进行比较,如果不同,则说明资源已经被修改,则返回200,如果相同则说明资源没有被修改,返回
304,客户端可以继续使用缓存。

HTTPS是如何保证安全的,证书如何校验?

HTTP如何实现长连接?

1
Connection:keep-alive

http1.1 之后 默认都是打开长连接的

tcp可靠连接的精髓:

TCP连接的一方A,由操作系统动态随机选取一个32位长的序列号(Initial+Sequence+Number),假设A的初始序列号为1000,以该序列号为原点,对自己将要发送的每个字节的数据进行编号,1001,1002,1003…,并把自己的初始序列号ISN告诉B,让B有一个思想准备,什么样编号的数据是合法的,什么编号是非法的,比如编号900就是非法的,同时B还可以对A每一个编号的字节数据进行确认。如果A收到B确认编号为2001,则意味着字节编号为1001-2000,共1000个字节已经安全到达。+同理B也是类似的操作,假设B的初始序列号ISN为2000,以该序列号为原点,对自己将要发送的每个字节的数据进行编号,2001,2002,2003…,并把自己的初始序列号ISN告诉A,以便A可以确认B发送的每一个字节。如果B收到A确认编号为4001,则意味着字节编号为2001-4000,共2000个字节已经安全到达。

一句话概括,TCP连接握手,握的是啥?通信双方数据原点的序列号!以此核心思想我们来分析二、三、四次握手的过程。

A<------->B四次握手的过程:

1
2
3
4
1. A 发送同步信号SYN A’s Initial sequence number 
2. B 确认收到A的同步信号,并记录 A‘s ISN 到本地,命名 B’s ACK sequence number
3. B发送同步信号SYN B‘s Initial sequence number
4. A确认收到B的同步信号,并记录 B’s ISN 到本地,命名 A‘s ACK sequence number 很显然1.2和1.3 这两个步骤可以合并,只需要三次握手,可以提高连接的速度与效率。

二次握手的过程:

1
2
1. A 发送同步信号SYN A’s Initial sequence number
2. B发送同步信号SYN B‘s Initial sequence number B’s ACK

sequence number 这里有一个问题,A与B就A的初始序列号达成了一致,这里是1000。但是B无法知道A是否已经接收到自己的同步信号,如果这个同步信号丢失了,A和B就B的初始序列号将无法达成一致。
于是TCP的设计者将SYN这个同步标志位SYN设计成占用一个字节的编号(FIN标志位也是),既然是一个字节的数据,按照TCP对有数据的TCP+segment+必须确认的原则,所以在这里A必须给B一个确认,以确认A已经接收到B的同步信号。 有童鞋会说,如果A发给B的确认丢了,该如何?

A会超时重传这个ACK吗?不会!TCP不会为没有数据的ACK超时重传
那该如何是好?B如果没有收到A的ACK,会超时重传自己的SYN同步信号,一直到收到A的ACK为止。


补充阅读:
第一个包,

即A发给B的SYN 中途被丢,没有到达B A会周期性超时重传,直到收到B的确认

第二个包,即B发给A的SYN BACK 中途被丢,没有到达A B会周期性超时重传,直到收到A的确认

第三个包,即A发给B的ACK 中途被丢,没有到达B A发完ACK,单方面认为TCP为 Established状态,而B显然认为TCP为Active状态:

1
2
3
4
5
a. 假定此时双方都没有数据发送,B会周期性超时重传,直到收到A的确认,收到之后B的TCP 连接也为 Established状态,双向可以发包。

b. 假定此时A有数据发送,B收到A的 Data+ACK,自然会切换为established 状态,并接受A的 Data。

C. 假定b有数据发送,数据发送不了,会一直周期性超时重传SYN +ACK,直到收到A的确认才可以发送数据。

应用层的数据不是直接发送给网卡的,

  1. linux系统有一个socket缓冲区,是一块物理内存,kernel将该物理地址的fd文件句柄透给用户空间,用户通过write(fd,stream)将二进制数字节流写入到socket缓冲区中,此时该数据片会被插入socket缓冲区的末尾,以保证数据的发送数据是先入先出的。
  2. socket缓冲区会关联一个叫做TCB的结构体,该结构体中存放了TCP链接所需要的全部数据,包括接受窗口,阻塞窗口,发送序号,重发计数器等。
  3. 在tcp层,如果满足发射条件,就会创建tcp 分段,(tcp segment)发送出去,但也有可能因为流量控制策略,系统决定不发包,调用就此停止。
  4. 进入IP层,在TCP分段中加入了IP信息,并进行IP路由,IP路由的目的是查找为了到达目的IP的要跳转的下一级IP地址。
  5. IP层增加了IP地址信息并进行IP路由之后,将数据发送到数据链路层,此时进行ARP获取目的地的mac地址信息。然后在数据端增加链路头信息。至此 tcp段的数据便是完整的了。
    1. 在接收到数据包传输请求之后,NIC把数据包从系统内存中拷贝到它自己的内存中,之后把数据包发送到网络上。在此时,由于要遵守以太网标准(Ethernet standard),NIC会向数据包中增加帧间隙(Inter-Frame Gap,IFG),同步码(preamble)和crc校验

所谓的长连接和短连接

对于HTTP 1.0的http标准而言,默认连接是短连接,啥叫短连接?就是服务器当发送完最后一个字节的数据之后将关闭连接,也就是回收tcp_sock结构,这样,如果客户端再发送数据给服务器,将直接丢弃。即使此时客户端还有这样的结构,但是我们说连接已经关闭或者已经断了。
那客户端知不知道啥时候服务器的连接关闭?不知道,双方可以在任何时候来关闭自己的连接而没有必要通知对方。不过,对于短连接而言,通知不通知也没有意义了。

那短连接的弊端,大家可能都已经知道了,如果对一个服务器要连续发送多个请求,还需要为每次请求建立新的连接。

为了降低建立连接的时间,HTTP 1.1引入了长连接的概念,并把它搞成了默认的连接方式。啥叫长连接?就是当完成一个业务之后,socket结构并不回收。这样,只要在socket结构还存在的时候,客户端发送的任何数据,服务器都可以收到,这就是所谓的长连接。

相比短连接而言,长连接并没有什么特别的新的技术,只是维护socket结构时间长了。因为,说http长连接更不如说是tcp长连接。 网卡会自动从该缓冲区取数据,在tcp层,首先通过write函数,

websocket

2、数据帧格式详解
针对前面的格式概览图,这里逐个字段进行讲解,如有不清楚之处,可参考协议规范,或留言交流。

FIN:1个比特。

如果是1,表示这是消息(message)的最后一个分片(fragment),如果是0,表示不是是消息(message)的最后一个分片(fragment)。

RSV1, RSV2, RSV3:各占1个比特。

一般情况下全为0。当客户端、服务端协商采用WebSocket扩展时,这三个标志位可以非0,且值的含义由扩展进行定义。如果出现非零的值,且并没有采用WebSocket扩展,连接出错。

Opcode: 4个比特。

操作代码,Opcode的值决定了应该如何解析后续的数据载荷(data payload)。如果操作代码是不认识的,那么接收端应该断开连接(fail the connection)。可选的操作代码如下:

1
2
3
4
5
6
7
8
9
%x0:表示一个延续帧。当Opcode为0时,表示本次数据传输采用了数据分片,当前收到的数据帧为其中一个数据分片。
%x1:表示这是一个文本帧(frame)
%x2:表示这是一个二进制帧(frame)
%x3-7:保留的操作代码,用于后续定义的非控制帧。
%x8:表示连接断开。
%x9:表示这是一个ping操作。
%xA:表示这是一个pong操作。
%xB-F:保留的操作代码,用于后续定义的控制帧。
Mask: 1个比特。

表示是否要对数据载荷进行掩码操作。从客户端向服务端发送数据时,需要对数据进行掩码操作;从服务端向客户端发送数据时,不需要对数据进行掩码操作。

如果服务端接收到的数据没有进行过掩码操作,服务端需要断开连接。

如果Mask是1,那么在Masking-key中会定义一个掩码键(masking key),并用这个掩码键来对数据载荷进行反掩码。所有客户端发送到服务端的数据帧,Mask都是1。

掩码的算法、用途在下一小节讲解。

Payload length:数据载荷的长度,单位是字节。为7位,或7+16位,或1+64位。

假设数Payload length === x,如果

x为0~126:数据的长度为x字节。
x为126:后续2个字节代表一个16位的无符号整数,该无符号整数的值为数据的长度。
x为127:后续8个字节代表一个64位的无符号整数(最高位为0),该无符号整数的值为数据的长度。
此外,如果payload length占用了多个字节的话,payload length的二进制表达采用网络序(big endian,重要的位在前)。

Masking-key:0或4字节(32位)

所有从客户端传送到服务端的数据帧,数据载荷都进行了掩码操作,Mask为1,且携带了4字节的Masking-key。如果Mask为0,则没有Masking-key。

备注:载荷数据的长度,不包括mask key的长度。

Payload data:(x+y) 字节

载荷数据:包括了扩展数据、应用数据。其中,扩展数据x字节,应用数据y字节。

扩展数据:如果没有协商使用扩展的话,扩展数据数据为0字节。所有的扩展都必须声明扩展数据的长度,或者可以如何计算出扩展数据的长度。此外,扩展如何使用必须在握手阶段就协商好。如果扩展数据存在,那么载荷数据长度必须将扩展数据的长度包含在内。

应用数据:任意的应用数据,在扩展数据之后(如果存在扩展数据),占据了数据帧剩余的位置。载荷数据长度 减去 扩展数据长度,就得到应用数据的长度。

3、掩码算法

掩码键(Masking-key)是由客户端挑选出来的32位的随机数。掩码操作不会影响数据载荷的长度。掩码、反掩码操作都采用如下算法:

首先,假设:

original-octet-i:为原始数据的第i字节。
transformed-octet-i:为转换后的数据的第i字节。
j:为i mod 4的结果。
masking-key-octet-j:为mask key第j字节。
算法描述为: original-octet-i 与 masking-key-octet-j 异或后,得到 transformed-octet-i。

j = i MOD 4
transformed-octet-i = original-octet-i XOR masking-key-octet-j

六、数据传递

一旦WebSocket客户端、服务端建立连接后,后续的操作都是基于数据帧的传递。

WebSocket根据opcode来区分操作的类型。比如0x8表示断开连接,0x0-0x2表示数据交互。

1、数据分片
WebSocket的每条消息可能被切分成多个数据帧。当WebSocket的接收方收到一个数据帧时,会根据FIN的值来判断,是否已经收到消息的最后一个数据帧。

FIN=1表示当前数据帧为消息的最后一个数据帧,此时接收方已经收到完整的消息,可以对消息进行处理。FIN=0,则接收方还需要继续监听接收其余的数据帧。

此外,opcode在数据交换的场景下,表示的是数据的类型。0x01表示文本,0x02表示二进制。而0x00比较特殊,表示延续帧(continuation frame),顾名思义,就是完整消息对应的数据帧还没接收完。

2、数据分片例子
直接看例子更形象些。下面例子来自MDN,可以很好地演示数据的分片。客户端向服务端两次发送消息,服务端收到消息后回应客户端,这里主要看客户端往服务端发送的消息。

第一条消息

FIN=1, 表示是当前消息的最后一个数据帧。服务端收到当前数据帧后,可以处理消息。opcode=0x1,表示客户端发送的是文本类型。

第二条消息

FIN=0,opcode=0x1,表示发送的是文本类型,且消息还没发送完成,还有后续的数据帧。
FIN=0,opcode=0x0,表示消息还没发送完成,还有后续的数据帧,当前的数据帧需要接在上一条数据帧之后。
FIN=1,opcode=0x0,表示消息已经发送完成,没有后续的数据帧,当前的数据帧需要接在上一条数据帧之后。服务端可以将关联的数据帧组装成完整的消息。
Client: FIN=1, opcode=0x1, msg=”hello”
Server: (process complete message immediately) Hi.
Client: FIN=0, opcode=0x1, msg=”and a”
Server: (listening, new message containing text started)
Client: FIN=0, opcode=0x0, msg=”happy new”
Server: (listening, payload concatenated to previous message)
Client: FIN=1, opcode=0x0, msg=”year!”
Server: (process complete message) Happy new year to you too!

七、连接保持+心跳

WebSocket为了保持客户端、服务端的实时双向通信,需要确保客户端、服务端之间的TCP通道保持连接没有断开。然而,对于长时间没有数据往来的连接,如果依旧长时间保持着,可能会浪费包括的连接资源。

但不排除有些场景,客户端、服务端虽然长时间没有数据往来,但仍需要保持连接。这个时候,可以采用心跳来实现。

发送方->接收方:ping
接收方->发送方:pong
ping、pong的操作,对应的是WebSocket的两个控制帧,opcode分别是0x9、0xA。

举例,WebSocket服务端向客户端发送ping,只需要如下代码(采用ws模块)

ws.ping(‘’, false, true);

项目相关:

HSM 状态机:

两个数组来模拟栈结构
一个临时数组 倒进 StateInfo数组 这样StateInfo里存的始终是当前激活路径的节点。

通过addState 构建State和StateInfo 的hashMap StateInfo里保存了父节点的信息。这样便构造了一颗树。

设置了初始节点状态后,调用StateMachine,start() 沿途激活的节点全部会进入enter(),同时设置active状态为true

当发生状态转移需要转换Stateinfo[] 中的节点时,会从目标转移节点出发向上递归,直到找到第一个stateInfo状态是active的节点为止。然后遍历旧的stateInfo[] 删除所有要出队的节点 调用exit方法。 加入要入对的节点,调用enter()
deferMessage会从后往前 依次出队 丢进handler的消息队列对头 然后由当前状态统一执行处理

频道状态维护:
initState

拉流数据准备 准备MediaVideoPlayer源 准备 mediaPlayer idle状态 ()

进入推拉流状态 进入MediaVideoPlayer使用状态 进入MediaPlayer状态

AOP 原理

javapoet可以写java文件
AOP 动态:本质是 拦截、代理、反射
静态 Aspectj :静态代理 编译时生成
1 使用ajc 编译器 (向下兼容java编译器,同时兼容aspect语法)编译.aj 文件后声成.java文件

  1. 因为android不支持ajc编译器,所以只能通过注解 和 aspecj的 aspectjweaver.jar包 通过匹配织入的语法规则 将目标代码插入到class文件中

注意 它编译的目标对象为.class 文件 执行时间为class转为dex之前。会对class文件进行重构
使用的是Javassist工具。通过在编译器 自定义Gradle插件和Transform API来完成对目标class对象的修改。

玩法逻辑

  1. 玩法通用 要想办法将配置抽象出来. 包括 等待匹配时长 领唱时长 抢麦最长时长 接唱时长 等待开始状态时长 结果展示时长 题目切换时长 抢麦结果展示时长等 在首次登录会拉取 进接唱匹配成功后也会随匹配通知带回。
  2. 进频道后 注册状态信息单播。 接受服务器消息 同时开启开始玩法超时计时器 等待所有玩家加入 超过配置时长未收到通知 会手动拉取。每次拉取后 会同步本地状态
  3. 每次进入特定状态后 都会启动超时计时器 如果未收到server的状态更新 会手动拉取。

将全局的游戏状态 使用LiveData进行维护的原因是 使得任何一次UI的状态推送,都可预期、都能方便地追溯来源,而不至于在 事件追溯复杂度为 n² 的迷宫中白费时间。

AssistendService

通过无障碍服务开启,获取该服务后 每次点击 焦点变换 视图树层级变化都会通知
同时可以通过getRootInActivitWindow()获取当前window的所有视图树层级

节点类型为AccessNodeInfo 里面有child对象 所以是一个树结构。
包含每个节点的bound[] 数据 是左右上下的坐标值。

内存优化:

除了基本的内存泄漏排查外 最主要是对bitmap内存进行了优化

本身bitmap的内存 = 单位像素大小 原图宽(像素数) 宽度缩放系数 原图高 (原图缩放系数)

首先是改进了屏幕适配方案 采用修改density值的方式 将density修改为设计稿360dp作为基准值。

总像素宽度 / density = 总宽度dp

所以为了保证 布局中的dp值 与基准设计稿 360dp的宽度百分比保持一致 就更改density值。

  1. 修改缩放值density: activityDm.density = activity.getResources().getDisplayMetrics.widthPixels/360 修目标
  2. activityDm.scaledDensity = activityDm.density * (systemDm.scaledDensity / systemDm.density);
  3. 修改dpi值: density * 160

如何保证只修改特定activigy 不改第三方库?
toast dailog 弹之前啊 aop hook方法 取消修改
通过aop只切入app自己的包 的 activity fragment等等

  1. 删除重复dpi文件夹下的重复资源,将其他分辨率文件下的资源删除,统一使用xhdpi文件夹

bitmap 当前dpi文件夹的density 与 目标设备density 相除 得到这个缩放值。

降低采样率

按需求大小加载图片

复用bitmap 其实就是Glide的加载策略。

网络优化

错误重试
自定义缓存策略
dns优化(ip直链)参考美团网络优化

CATALOG
  1. 1. view 绘制流程相关:
    1. 1.0.1. 如何在activity中正确获取view的宽高:
    2. 1.0.2. setWillNotDraw()
    3. 1.0.3. 当子view的measureSpec的type是unspecified时
    4. 1.0.4. layout过程
    5. 1.0.5. MotionEvent()
    6. 1.0.6. view 的事件分发
    7. 1.0.7. requestLayout、invalidate与postInvalidate
      1. 1.0.7.1. requestLayout:
      2. 1.0.7.2. invalidata
      3. 1.0.7.3. postinvalidata
  • 2. activity的启动流程
    1. 2.1. 热更新
    2. 2.2. 网络请求框架:
    3. 2.3. 换肤
    4. 2.4. lrucache的实现原理
    5. 2.5. crash的相关处理:
      1. 2.5.1. java crash
      2. 2.5.2. native crash
    6. 2.6. JNI技术相关
    7. 2.7. 并发相关
    8. 2.8. 并发相关的集合:
    9. 2.9. 并发包中 锁机制的实现
    10. 2.10. 并发的底层实现:
      1. 2.10.1. volatile
        1. 2.10.1.1. 1.可见性的保证:
        2. 2.10.1.2. 2 访问一致性:(禁止指令重拍)
      2. 2.10.2. syncornized
      3. 2.10.3. Unsafe对象
      4. 2.10.4. CAS原理:
        1. 2.10.4.1. 原理
        2. 2.10.4.2. CAS缺点
      5. 2.10.5. SAQ模型
    11. 2.11. 并发的实战问题
    12. 2.12. MVVM 架构的演进
    13. 2.13. Rxjava相关
      1. 2.13.0.1. rxjava 的线程切换
      2. 2.13.0.2. 关键成员
        1. 2.13.0.2.1. Subscriber:
        2. 2.13.0.2.2. Scheduler 线程调度
        3. 2.13.0.2.3. map()函数:
        4. 2.13.0.2.4. interval()函数
        5. 2.13.0.2.5. rxjava 如何保证串行发射的
      3. 2.13.0.3. 高级方法: backPressure 背压
        1. 2.13.0.3.1. rxjava2
        2. 2.13.0.3.2. flatMap()
        3. 2.13.0.3.3. 线程池:
  • 2.14. Glide
    1. 2.14.1. 1. Glide的构建流程
    2. 2.14.2. Glide是如何完成生命周期感知的
    3. 2.14.3. Glide的双缓存
    4. 2.14.4. 为什么双缓存里 在内存缓存之前 还要有一个HashMap
    5. 2.14.5. Glide gif图优化
    6. 2.14.6. 缓存策略:
    7. 2.14.7. Glide的bitmap池复用
    8. 2.14.8. Glide 对图片格式的判断方法
  • 2.15. Retrofit
    1. 2.15.1. 1. 首先构造Retrofit对象
    2. 2.15.2. 2. 通过注解定义网络请求的Api,并且创建了请求服务的实体对象。
    3. 2.15.3. 3. 调用请求接口:
      1. 2.15.3.1. 1. 创建ServiceMethod
      2. 2.15.3.2. 2. 创建okHttpCall
        1. 2.15.3.2.1. a 创建okhttp3.call
        2. 2.15.3.2.2. b 同步请求网络并转换结果
        3. 2.15.3.2.3. c 异步请求
      3. 2.15.3.3. 3. callAdapter 将okhttpCall 转换为我们想要的call类型
    4. 2.15.4. 开始网路请求
    5. 2.15.5. retrofit里丰富的设计模式:
  • 2.16. OkHttp3
    1. 2.16.1. 1. 创建HttpClient对象
    2. 2.16.2. 2. Builder里做了什么
    3. 2.16.3. 3. 创建request
    4. 2.16.4. 创建RealCall对象
    5. 2.16.5. 同步请求网络 call.execute()
    6. 2.16.6. getResponseWithInterceptorChain 请求网并处理数据
    7. 2.16.7. 总结
      1. 2.16.7.1. 构造OkHttpClient 并初始化分发器
      2. 2.16.7.2. 构造Request对象
      3. 2.16.7.3. 分发器决定同步还是异步执行
      4. 2.16.7.4. 自定义拦截器请求网络并获取response
      5. 2.16.7.5. Interceptors
      6. 2.16.7.6. HttpCodec
        1. 2.16.7.6.1. StreamAllocation
        2. 2.16.7.6.2. RetryAndFollowUpInterceptor
        3. 2.16.7.6.3. BridgeInterceptor
        4. 2.16.7.6.4. CacheInterceptor
      7. 2.16.7.7. connectInterceptor
        1. 2.16.7.7.1. CallServerInterceptor
      8. 2.16.7.8. okhttp 使用websocket
      9. 2.16.7.9. okhttp的连接池复用:
    8. 2.16.8. okhttp的链接池清除机制:
    9. 2.16.9. okhttp如何判断一个链接是健康的
    10. 2.16.10. okhttp如何判断一个connection可复用
    11. 2.16.11. okhttp的无上界线城池
    12. 2.16.12. okhttp 三个重要的类:
  • 2.17. apk前签名机制:
    1. 2.17.1. apk签名
      1. 2.17.1.1. 1.MAINFEST.MF
      2. 2.17.1.2. 2. CERT.SF
      3. 2.17.1.3. 3. CERT.RSA
    2. 2.17.2. 安装验证
  • 2.18. java基础:
    1. 2.18.1. 类的初始化 (主动引用 被动引用)
    2. 2.18.2. 初始化次序:
    3. 2.18.3. class文件从解析到生成对象的全过程:
      1. 2.18.3.1. 1. java编译器将java文件编译为class文件
      2. 2.18.3.2. 2. jvm运行时数据区
  • 2.19. java 集合相关:
    1. 2.19.1. hashMap
    2. 2.19.2. concurrentHashMap
    3. 2.19.3. hashSet
    4. 2.19.4. hashTable
    5. 2.19.5. LinkedHashMap
    6. 2.19.6. LinkedHashSet
    7. 2.19.7. ArrayList
    8. 2.19.8. Vector
    9. 2.19.9. LinkedList
    10. 2.19.10. SparseArray
    11. 2.19.11. CopyOnWriteArrayList
  • 2.20. 虚拟机相关 JVM–> Divalk 虚拟机 –> ART虚拟机
    1. 2.20.1. JVM
      1. 2.20.1.1. 1. 编译java代码
      2. 2.20.1.2. a: 常量池的类型包括:
        1. 2.20.1.2.1. b:访问标记:
        2. 2.20.1.2.2. c:fields counts 和field
        3. 2.20.1.2.3. d :methods
      3. 2.20.1.3. 加载class字节码
      4. 2.20.1.4. 链接
      5. 2.20.1.5. 初始化
    2. 2.20.2. JIT AOT JAVAC 编译器区别
    3. 2.20.3. AOT
    4. 2.20.4. android 历史各版本的编译器演进:
    5. 2.20.5. dv虚拟机 对比jvm虚拟机
      1. 2.20.5.1. 手画一下Android系统架构图,描述一下各个层次的作用?
    6. 2.20.6. Activity如与Service通信?
    7. 2.20.7. Service的生命周期与启动方法由什么区别?
    8. 2.20.8. Service先start再bind如何关闭service,为什么bindService可以跟Activity生命周期联动?
    9. 2.20.9. 广播分为哪几种,应用场景是什么?
    10. 2.20.10. 广播的两种注册方式有什么区别?
    11. 2.20.11. 广播发送和接收的原理了解吗?
    12. 2.20.12. 广播传输的数据是否有限制,是多少,为什么要限制?
    13. 2.20.13. ContentProvider、ContentResolver与ContentObserver之间的关系是什么?
    14. 2.20.14. 遇到过哪些关于Fragment的问题,如何处理的?
    15. 2.20.15. Android里的Intent传递的数据有大小限制吗,如何解决?
    16. 2.20.16. 描述一下Android的事件分发机制?
    17. 2.20.17. 描述一下View的绘制原理?
    18. 2.20.18. requestLayout()、invalidate()与postInvalidate()有什么区别?
    19. 2.20.19. Scroller用过吗,了解它的原理吗?
    20. 2.20.20. 了解APK的打包流程吗,描述一下?
    21. 2.20.21. 了解APK的安装流程吗,描述一下?
    22. 2.20.22. 当点击一个应用图标以后,都发生了什么,描述一下这个过程?
    23. 2.20.23. BroadcastReceiver与LocalBroadcastReceiver有什么区别?
    24. 2.20.24. Android Handler机制是做什么的,原理了解吗?
    25. 2.20.25. Android Binder机制是做什么的,为什么选用Binder,原理了解吗?
    26. 2.20.26. 描述一下Activity的生命周期,这些生命周期是如何管理的?
    27. 2.20.27. Activity的通信方式有哪些?
    28. 2.20.28. Android应用里有几种Context对象,
    29. 2.20.29. 描述一下进程和Application的生命周期?
    30. 2.20.30. Android哪些情况会导致内存泄漏,如何分析内存泄漏?
    31. 2.20.31. Android有哪几种进程,是如何管理的?
    32. 2.20.32. SharePreference性能优化,可以做进程同步吗?
    33. 2.20.33. 如何做SQLite升级?
    34. 2.20.34. 进程保护如何做,如何唤醒其他进程?
    35. 2.20.35. 理解序列化吗,Android为什么引入Parcelable?
    36. 2.20.36. 如何计算一个Bitmap占用内存的大小,怎么保证加载Bitmap不产生内存溢出?
    37. 2.20.37. Android如何在不压缩的情况下加载高清大图?
    38. 2.20.38. Android里的内存缓存和磁盘缓存是怎么实现的。
    39. 2.20.39. PathClassLoader与DexClassLoader有什么区别?
    40. 2.20.40. WebView优化了解吗,如何提高WebView的加载速度?
    41. 2.20.41. Java和JS的相互调用怎么实现,有做过什么优化吗?
    42. 2.20.42. JNI了解吗,Java与C++如何相互调用?
    43. 2.20.43. 了解插件化和热修复吗,它们有什么区别,理解它们的原理吗?
    44. 2.20.44. 如何做性能优化?
    45. 2.20.45. 如果防止过度绘制,如何做布局优化?
    46. 2.20.46. 如何提交代码质量?
    47. 2.20.47. 有没有遇到64k问题,为什么,如何解决?
    48. 2.20.48. MVC、MVP与MVVM之间的对比分析?
  • 2.21. 网络编程
    1. 2.21.1. TCP与UDP有什么区别?
    2. 2.21.2. 简单介绍一下TCP三次握手与四次分手过程?
    3. 2.21.3. TCP如何保证数据传输的可靠性?
    4. 2.21.4. HTTP与HTTPS有什么区别?
    5. 2.21.5. 谈一谈对HTTP缓存的理解?
    6. 2.21.6. HTTPS是如何保证安全的,证书如何校验?
    7. 2.21.7. HTTP如何实现长连接?
    8. 2.21.8. tcp可靠连接的精髓:
      1. 2.21.8.1. AB四次握手的过程:
      2. 2.21.8.2. 二次握手的过程:
      3. 2.21.8.3. 所谓的长连接和短连接
      4. 2.21.8.4. websocket
      5. 2.21.8.5. 3、掩码算法
      6. 2.21.8.6. 六、数据传递
        1. 2.21.8.6.1. 七、连接保持+心跳
    9. 2.21.9. 项目相关:
      1. 2.21.9.1. HSM 状态机:
      2. 2.21.9.2. AOP 原理
      3. 2.21.9.3. 玩法逻辑
      4. 2.21.9.4. AssistendService
      5. 2.21.9.5. 内存优化:
      6. 2.21.9.6. 网络优化