Android进阶知识点

作者: wayDevelop | 来源:发表于2019-01-09 13:49 被阅读101次

    HashMap、Hashtable、ConcurrentHashMap的原理与区别

    HashMap、Hashtable、ConcurrentHashMap的原理与区别

    • HashMap
      底层数组+链表实现,可以存储null键和null值,线程不安全
      初始size为16,扩容:newsize = oldsize*2,size一定为2的n次幂
      扩容针对整个Map,每次扩容时,原来数组中的元素依次重新计算存放位置,并重新插入
      插入元素后才判断该不该扩容,有可能无效扩容(插入后如果扩容,如果没有再次插入,就会产生无效扩容)
      当Map中元素总数超过Entry数组的75%,触发扩容操作,为了减少链表长度,元素分配更均匀
      计算index方法:index = hash & (tab.length – 1)
      HashMap有几个重要的参数:负载因子和初始容量

    如果HashMap的大小超过了负载因子定义的容量,HashMap将会创建一个原来两倍的bucket数组,将原来的对象放入新的数组中,扩大hashMap的容量。(负载因子初始0.75)
    初始容量:表示HashMap的初始大小,也就是默认大小。
    负载因子:当HashMap容量不够的时候需要扩容,负载因子就是限定扩容的条件,默认是0.75,即达到默认/初次指定容量的75%就扩容,每次扩容2倍,即<< 1

    • HashMap为什么线程不安全
      如上,在两个线程同时尝试扩容HashMap时,可能将一个链表形成环形的链表,所有的next都不为空,进入死循环
      在两个线程同时进行put时可能造成一个线程数据的丢失
    • HashTable
      底层数组+链表实现,无论key还是value都不能为null,线程安全,实现线程安全的方式是在修改数据时锁住整个HashTable,效率低,ConcurrentHashMap做了相关优化
      初始size为11,扩容:newsize = olesize*2+1
      计算index的方法:index = (hash & 0x7FFFFFFF) % tab.length
    • ConcurrentHashMap
      底层采用分段的数组+链表实现,线程安全
      通过把整个Map分为N个Segment,可以提供相同的线程安全,但是效率提升N倍,默认提升16倍。(读操作不加锁,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。)
      Hashtable的synchronized是针对整张Hash表的,即每次锁住整张表让线程独占,ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术
      有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁
      扩容:段内扩容(段内元素超过该段对应Entry数组长度的75%触发扩容,不会对整个Map进行扩容),插入前检测需不需要扩容,有效避免无效扩容

    Binder的运行机制

    Binder
    Android系统中,涉及到多进程间的通信底层都是依赖于Binder IPC机制。例如当进程A中的Activity要向进程B中的Service通信,这便需要依赖于Binder IPC。不仅于此,整个Android系统架构中,大量采用了Binder机制作为IPC(进程间通信)方案。

    • 为什么用binder

    性能方面(Binder数据拷贝只需要一次,而管道、消息队列、Socket都需要2次)
    安全方面(Binder机制从协议本身就支持对通信双方做身份校检,因而大大提升了安全性)


    对于用户空间,不同进程之间彼此是不能共享的,而内核空间却是可共享的。Client进程向Server进程通信,恰恰是利用进程间可共享的内核内存空间来完成底层通信工作的,Client端与Server端进程往往采用ioctl等方法跟内核空间的驱动进行交互。

    • Binder原理
      Binder通信采用client-server架构,从组件视角来说,包含Client、Server、ServiceManager以及binder驱动,其中ServiceManager用于管理系统中的各种服务。架构图如下所示:



      Binder 机制

    • 首先需要注册服务端,只有注册了服务端,客户端才有通讯的目标,服务端通过 ServiceManager 注册服务,注册的过程就是向 Binder 驱动的全局链表 binder_procs 中插入服务端的信息,然后向 ServiceManager 的 svcinfo 列表中缓存一下注册的服务。
    • 有了服务端,客户端就可以跟服务端通讯了,须先向ServiceManager中获取相应的Service,
    • 有了服务端的引用我们就可以向服务端发送请求了,通过 BinderProxy 将我们的请求参数发送给 ServiceManager,通过共享内存的方式使用内核方法 copy_from_user() 将我们的参数先拷贝到内核空间,这时我们的客户端进入等待状态,然后 Binder 驱动向服务端的 todo 队列里面插入一条事务,执行完之后把执行结果通过 copy_to_user() 将内核的结果拷贝到用户空间(这里只是执行了拷贝命令,并没有拷贝数据,binder只进行一次拷贝),唤醒等待的客户端并把结果响应回来,这样就完成了一次通讯。

    ClassLoader类加载器

    ClassLoader的具体作用就是将class文件加载到jvm虚拟机中去,程序就可以正确运行了。但是,jvm启动的时候,并不会一次性加载所有的class文件,而是根据需要去动态加载。想想也是的,一次性加载那么多jar包那么多class,那内存不崩溃。

    热修复原理 andfix

    第一步 在服务器生成修复包,打包成dex 给用户下载
    第二步
    app客户端加载dex包
    第三步
    找到修复好的class 找到修复好的方法

    使用注解标识需要修复的类和方法
    通过DexFile 加载手机里面的dex文件
    服务器编程方法 需要指定包名和方法,让他知道 需要去修复那个地方。其实这一步就是制造补丁包dex
    ***class 怎么打包成class?
    sdk目录build_tool 下的dx.bat工具

    Thinker

    image.png

    思路:从服务器下载修复好的dex包如(class1.dex),替换原有bug的dex

    • 注意事项
      1,主包不能有bug 不然启动就闪退,不能加载dex
      2,必须是运行的时候

    • 补丁包如何生成?
      dx.bat 打包dex,

    说起热修复就不得不提类的加载机制,和常规的JVM类似,在Android中类的加载也是通过ClassLoader来完成,具体来说就是PathClassLoader 和 DexClassLoader 这两个Android专用的类加载器,这两个类的区别如下:

    PathClassLoader:只能加载已经安装到Android系统中的apk文件(/data/app目录),是Android默认使用的类加载器。
    DexClassLoader:可以加载任意目录下的dex/jar/apk/zip文件,也就是我们一开始提到的补丁。

    这两个类都是继承自BaseDexClassLoader,我们可以看一下BaseDexClassLoader的构造函数

    public BaseDexClassLoader(String dexPath, File optimizedDirectory,
                String libraryPath, ClassLoader parent) {
            super(parent);
            this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
        }
    

    这个构造函数只做了一件事,就是通过传递进来的相关参数,初始化了一个DexPathList对象。DexPathList的构造函数,就是将参数中传递进来的程序文件(就是补丁文件)封装成Element对象,并将这些对象添加到一个Element的数组集合dexElements中去。
    可能有多个dex 使用hashset存放需要修复的dex集合

    我们现在要去查找一个名为name的class,那么DexClassLoader将通过以下步骤实现:

    在DexClassLoader的findClass 方法中通过一个DexPathList对象findClass()方法来获取class
    在DexPathList的findClass 方法中,对之前构造好dexElements数组集合进行遍历,一旦找到类名与name相同的类时,就直接返回这个class,找不到则返回null。

    总的来说,通过DexClassLoader查找一个类,最终就是就是在一个数组中查找特定值的操作。
    综合以上所有的观点,我们很容易想到一种非常简单粗暴的热修复方案。假设现在代码中的某一个类或者是某几个类有bug,那么我们可以在修复完bug之后,可以将这些个类打包成一个补丁文件,然后通过这个补丁文件封装出一个Element对象,并且将这个Element对象插到原有dexElements数组的最前端,这样当DexClassLoader去加载类时,优先会从我们插入的这个Element中找到相应的类,虽然那个有bug的类还存在于数组中后面的Element中,但由于双亲加载机制的特点,这个有bug的类已经没有机会被加载了,这样一个bug就在没有重新安装应用的情况下修复了。

    Robust

    Robust插件对每个产品代码的每个函数都在编译打包阶段自动的插入了一段代码,插入过程对业务开发是完全透明
    为每个class增加了个类型为ChangeQuickRedirect的静态成员,而在每个方法前都插入了使用changeQuickRedirect相关的逻辑,当 changeQuickRedirect不为null时,可能会执行到accessDispatch从而替换掉之前老的逻辑,达到fix的目的。

    1. 集成了 Robust 后,生成 apk。保存期间的混淆文件 mapping.txt,以及 Robust 生成记录文件 methodMap.robust
    2. 使用注解 @Modify 或者方法 RobustModify.modify() 标注需要修复的方法
    3. 开启补丁插件,执行生成 apk 命令,获得补丁包 patch.jar
    4. 通过推送或者接口的形式,通知 app 有补丁,需要修复
    5. 加载补丁文件不需要重新启动应用
      该文件在打补丁的时候用来区别到底哪些方法需要被修复,所以有它才能打补丁。而上文所说的还有 mapping.txt 文件,该文件列出了原始的类,方法和字段名与混淆后代码间的映射。这个文件很重要,可以用它来翻译被混淆的代码。但

    插件化的原理

    通过上面的框架介绍,插件化的原理无非就是这些:
    在Android中应用插件化技术,其实也就是动态加载的过程,分为以下几步:
    通过DexClassLoader加载。把可执行文件( .so/dex/jar/apk 等)拷贝到应用 APP 内部。
    加载可执行文件,更换静态资源
    调用具体的方法执行业务逻辑

    View和ViewGroup的区别:

    可以从两方面来说:
    一.事件分发方面的区别;
    二.UI绘制方面的区别;

    UI绘制主要有五个方法:onDraw(),onLayout(),onMeasure(),dispatchDraw(),drawChild()
    1.ViewGroup包含这五个方法,而View只包含onDraw(),onLayout(),onMeasure()三个方法,不包含dispatchDraw(),drawChild()。

    Android的UI界面都是由View和ViewGroup及其派生类组合而成的。其中,View是所有UI组件的基类,而ViewGroup是容纳View及其派生类的容器,ViewGroup也是从View派生出来的。一般来说,开发UI界面都不会直接使用View和ViewGroup(自定义控件的时候使用),而是使用其派生类。

    性能优化

    快:使用时避免出现卡顿,响应速度快,减少用户等待的时间,满足用户期望。
    (UI 绘制、应用启动、页面跳转、事件响应,
    界面绘制。主要原因是绘制的层级深、页面复杂、刷新不合理,由于这些原因导致卡顿的场景更多出现在 UI 和启动后的初始界面以及跳转到页面的绘制上。
    数据处理。导致这种卡顿场景的原因是数据处理量太大,一般分为三种情况,一是数据在处理 UI 线程,二是数据处理占用 CPU 高,导致主线程拿不到时间片,三是内存增加导致 GC 频繁,从而引起卡顿。)

    在手机开发者模式下,有一个卡顿检测工具叫做:Profile GPU Rendering,一个图形监测工具,能实时反应当前绘制的耗时
    TraceView以分析到每一个方法的执行时间,

    稳:减低 crash 率和 ANR 率,不要在用户使用过程中崩溃和无响应。
    (提高代码质量。比如开发期间的代码审核,看些代码设计逻辑,业务合理性等。
    代码静态扫描工具。常见工具有Android Lint、Findbugs、Checkstyle、PMD等等。
    Crash监控。把一些崩溃的信息,异常信息及时地记录下来,以便后续分析解决。
    Crash上传机制。在Crash后,尽量先保存日志到本地,然后等下一次网络正常时再上传日志信息。)

    省:节省流量和耗电,减少用户使用成本,避免使用时导致手机发烫。
    (:Battery Historian。Battery Historian 是一款由 Google 提供的 Android 系统电量分析工具)

    小:安装包小可以降低用户的安装成本。
    代码混淆。资源优化,图片优化,避免重复功能的库,cdn

    android内存的优化

    android内存泄露容易导致内存溢出,又称为OOM。
    Android内存优化策略:
    1)在循环内尽量不要使用局部变量
    2)不用的对象即时释放,即指向NULL
    3)数据库的cursor即时关闭。
    4)构造adapter时使用缓存contentview
    5)调用registerReceiver()后在对应的生命周期方法中调用unregisterReceiver()
    6)即时关闭InputStream/OutputStream。
    7)android系统给图片分配的内存只有8M, 图片尽量使用软引用, 较大图片可通过BitmapFactory缩放后再使用,并及时recycle
    8)尽量避免static成员变量引用资源耗费过多的实例。
    加载大图片的时候如何防止内存溢出
    答: android系统给图片分配的内存只有8M,当加载大量图片时往往会出现OOM。

    Android加载大量图片内存溢出解决方案:

    1)尽量不要使用setImageBitmap或setImageResource或BitmapFactory.decodeResource来设置一张大图,因为这些函数在完成decode后,最终都是通过java层的createBitmap来完成的,需要消耗更多内存,可以通过BitmapFactory.decodeStream方法,创建出一个bitmap,再将其设为ImageView的 source
    2)使用BitmapFactory.Options对图片进行压缩
    InputStream is = this.getResources().openRawResource(R.drawable.pic1);
    BitmapFactory.Options options=new BitmapFactory.Options();
    options.inJustDecodeBounds = false;
    options.inSampleSize = 10; //width,hight设为原来的十分一
    Bitmap btp =BitmapFactory.decodeStream(is,null,options);
    3)运用Java软引用,进行图片缓存,将需要经常加载的图片放进缓存里,避免反复加载
    及时销毁不再使用的Bitmap对象
    if(!bmp.isRecycle() ){
    bmp.recycle() //回收图片所占的内存
    system.gc() //提醒系统及时回收
    }

    深度优先搜索和广度优先搜索的区别

    深度优先搜素算法:类似先序遍历,不全部保留结点,占用空间少;有回溯操作(即有入栈、出栈操作),运行速度慢。
    广度优先搜索算法:保留全部结点,占用空间大; 无回溯操作(即无入栈、出栈操作),运行速度快。
    广度优先遍历:又叫层次遍历,从上往下对每一层依次访问,在每一层中,从左往右(也可以从右往左)访问结点,访问完一层就进入下一层,直到没有结点可以访问为止。

    两种方法最大的区别在于前者从顶点的第一个邻接点一直访问下去再访问顶点的第二个邻接点;后者从顶点开始访问该顶点的所有邻接点再依次向下,一层一层的访问。

    Java中常见的锁类型

    常见的锁分类大致有:排它锁、共享锁、乐观锁、悲观锁、分段锁、自旋锁、公平锁、非公平锁、可重入锁等。

    是Java多线程加锁机制,有两种:
    1,Synchronized synchronized是一种互斥锁 一次只能允许一个线程进入被锁住的代码块
    当方法(代码块)执行完毕后会自动释放锁,不需要做任何的操作。
    当一个线程执行的代码出现异常时,其所持有的锁会自动释放。

    2,显式Lock

    可以简单概括一下:
    Lock方式来获取锁支持中断、超时不获取、是非阻塞的
    提高了语义化,哪里加锁,哪里解锁都得写出来
    Lock显式锁可以给我们带来很好的灵活性,但同时我们必须手动释放锁
    支持Condition条件对象
    允许多个读线程同时访问共享资源

    死锁原理

    根据操作系统中的定义:死锁是指在一组进程中的各个进程均占有不会释放的资源,但因互相申请被其他进程所站用不会释放的资源而处于的一种永久等待状态。

    死锁的四个必要条件:
    互斥条件(Mutual exclusion):资源不能被共享,只能由一个进程使用。
    请求与保持条件(Hold and wait):已经得到资源的进程可以再次申请新的资源。
    非剥夺条件(No pre-emption):已经分配的资源不能从相应的进程中被强制地剥夺。
    循环等待条件(Circular wait):系统中若干进程组成环路,该环路中每个进程都在等待相邻进程正占用的资源。

    一、死锁的定义
    所谓死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。

    处理死锁的基本方法

    1.预防死锁:通过设置一些限制条件,去破坏产生死锁的必要条件

    2.避免死锁:在资源分配过程中,使用某种方法避免系统进入不安全的状态,从而避免发生死锁

    3.检测死锁:允许死锁的发生,但是通过系统的检测之后,采取一些措施,将死锁清除掉

    4.解除死锁:该方法与检测死锁配合使用

    Anr的主要原因

    ANR一般有三种类型:
    1:KeyDispatchTimeout(5 seconds) --主要类型
    按键或触摸事件在特定时间内无法得到响应
    2:BroadcastTimeout(10 seconds)
    BroadcastReceiver在的onRecieve运行在主线程中,短时间内无法处理完成导致
    3:ServiceTimeout(20 seconds) --小概率类型
    Service的各个声明周期在特定时间内无法处理完成
    ANR产生时,系统会生成一个traces.txt的文件放在/data/anr/下。)使用命令导出anr日志
    adb pull /data/anr/traces.txt ~/Desktop/
    分析关键信息
    以每行的重点内容没准,每行自带时间戳
    因为主线程被阻塞导致的关键信息。
    为什么会超时:事件没有机会处理 & 事件处理超时
    怎么避免ANR
    ANR的关键
    是处理超时,所以应该避免在UI线程,BroadcastReceiver 还有service主线程中,处理复杂的逻辑和计算
    而交给work thread操作。
    1)避免在activity里面做耗时操作,oncreate & onresume
    2)避免在onReceiver里面做过多操作
    3)避免在Intent Receiver里启动一个Activity,因为它会创建一个新的画面,并从当前用户正在运行的程序上抢夺焦点。
    4)尽量使用handler来处理UI thread & workthread的交互。

    常见的内存泄漏。

    单例造成的内存泄漏
    非静态内部类创建静态实例造成的内存泄漏
    线程造成的内存泄漏
    资源未关闭造成的内存泄漏
    Handler造成的内存泄漏
    一些建议
    对于生命周期比Activity长的对象如果需要应该使用ApplicationContext
    对于需要在静态内部类中使用非静态外部成员变量(如:Context、View ),可以在静态内部类中使用弱引用来引用外部类的变量来避免内存泄漏
    对于不再需要使用的对象,显示的将其赋值为null,比如使用完Bitmap后先调用recycle(),再赋为null
    保持对对象生命周期的敏感,特别注意单例、静态对象、全局性集合等的生命周期
    对于生命周期比Activity长的内部类对象,并且内部类中使用了外部类的成员变量,可以这样做避免内存泄漏:
    将内部类改为静态内部类
    静态内部类中使用弱引用来引用外部类的成员变量
    在涉及到Context时先考虑ApplicationContext,当然它并不是万能的,对于有些地方则必须使用Activity的Context,对于Application,Service,Activity三者的Context的应用场景如下:

    内存模型

    1、虚拟机栈
    2、本地方法栈
    3、PC 寄存器
    4、堆
    5、方法区

    Java的堆内存和栈内存

    Java把内存划分成两种:一种是堆内存,一种是栈内存。

    堆:主要用于存储实例化的对象,数组。由JVM动态分配内存空间。一个JVM只有一个堆内存,线程是可以共享数据的。

    栈:主要用于存储局部变量和对象的引用变量,每个线程都会有一个独立的栈空间,所以线程之间是不共享数据的。
    栈内存的更新速度要快于堆内存,因为局部变量的生命周期更短,
    栈内存存放的变量生命周期一旦结束就会被释放,而堆内存存放的会被垃圾回收期机制不定时的回收。

    GC机制判断对象是否存活的算法:

    1、引用计数算法
    给对象添加一个引用计数器,当有一个地方引用它时,计数器值就加1;当引用失效时,计数器就减1;任何时候计数器都为0的对象就是不可能再被使用的。引用计数器算法(Reference Counting)实现简单,判定效率也很高,在大部分情况下他都是一个不错的算法。但是,Java语言中没有选用引用计数算法来管理内存,其中最主要的原因是他很难解决对象之间的互相循环引用的问题。

    2、根搜索算法
    根搜索算法(GC Roots Tracing)的基本思路是通过一系列名为“GC Roots”的对象作为起始点,从这个节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论术语描述就是从GC Roots到这个对象不可达)时,则证明此对象是不可用的。在主流的商用程序语言中(Java、C#),都是使用根搜索算法判定对象是否存活的。

    常见回收算法

    1、标记-清除算法:

    标记阶段:先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象;

    清除阶段:清除所有未被标记的对象。

    2、复制算法:(新生代的GC)

    将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,然后清除正在使用的内存块中的所有对象。

    3、标记-整理算法:(老年代的GC)

    标记阶段:先通过根节点,标记所有从根节点开始的可达对象。因此,未被标记的对象就是未被引用的垃圾对象

    整理阶段:将将所有的存活对象压缩到内存的一端;之后,清理边界外所有的空间

    4、分代收集算法:

    存活率低:少量对象存活,适合复制算法:在新生代中,每次GC时都发现有大批对象死去,只有少量存活(新生代中98%的对象都是“朝生夕死”),那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成GC。

    存活率高:大量对象存活,适合用标记-清理/标记-整理:在老年代中,因为对象存活率高、没有额外空间对他进行分配担保,就必须使用“标记-清理”/“标记-整理”算法进行GC。

    Android程序Crash时的异常上报

    https://blog.csdn.net/singwhatiwanna/article/details/17289479

    android中有处理这类问题的方法,请看下面Thread类中的一个方法#setDefaultUncaughtExceptionHandler
    新建一个类,比如叫CrashHandler.java实现UncaughtExceptionHandler
    为ui线程添加默认异常事件Handler,我们推荐大家在Application中添加而不是在Activity中添加。Application标识着整个应用,在Android声明周期中是第一个启动的,早于任何的Activity、Service等。

    当crash发生的时候,我们可以捕获到异常信息,把异常信息存储到SD卡中,然后在合适的时机通过网络将crash信息上传到服务器上,这样开发人员就可以分析用户crash的场景从而在后面的版本中修复此类crash。我们还可以在crash发生时,弹出一个通知告诉用户程序crash了,然后再退出,这样做比闪退要温和一点。

    Android中ListView常见优化方案

    1、ViewHolder模式提高效率
    Viewholder模式利用了ListView的视图缓存机制,避免了每次在调用getView的时候都去通过findViewById实例化数据。
    2、耗时操作放到异步线程中
    比如说:加载图片
    3、item错位
    由于耗时操作,而且又用到了view的复用,可能会出现item错位
    解决错位方法:可以为每一个item设置一个tag
    4、加载数据量大的数据
    1.设置本地缓存
    2.分页加载:我们不用每次把ListView所有的Item都一次性加载完毕,这样做没必要也很累。我们仅仅需要加载那部分显示在屏幕部分的Item即可,这样所要加载的数据就减少了很多
    3、滑动时停止加载:当用户滑动时,显示在屏幕的Item会不断的变化,如果只是加载显示在屏幕的Item,这也没有必要,因此我们应该在停止滑动时再加载数据。

    冒泡排序优化

    第一种优化方式是(内层循环优化)设置一个标记位,
    定义一个flag=0,用来判断有没有进行交换,如果在某次内层循环中没有交换操作,就说明此时数组已经是有序了的,不用再进行判断,这样可以节省时间

    第二种 (外层循环优化)如果用一个flag来判断一下,当前数组是否已经有序,如果有序就退出循环,这样可以明显的提高冒泡排序的性能

    Android的wrap_content是如何计算的

    先处理wrap_content,简单说:在View类中,当该View的布局宽高值为wrap_content,或match_parent时,该View测量的最终大小就是MeasureSpec中的测量大小-->SpecSize。因此,在自定义View时,需要重写onMeasure(w,h)用来处理wrap_content的情况,然后调用setMeasuredDimession(w,h)完成测量。

    当布局宽高为wrap_content时,它的测量模式specMode必然为AT_MOST。于是取出宽高的测量模式进行判断,如果布局宽高的不是wrap_content时,就按照View$onMeasure(w,h)中方式处理,也就是用父view给的建议测量大小specSize,作为最终测量大小值。重写onMeasure(w,h)后执行程序,

    EventBus

    关于观察者模式
    简介:观察者模式是设计模式中的一种。它是为了定义对象间的一种一对多的依赖关系,即当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。

    在这个模式中主要包含两个重要的角色:发布者和订阅者(又称观察者)。对应EventBus来说,发布者即发送消息的一方(即调用EventBus.getDefault().post(event)的一方)
    ,订阅者即接收消息的一方(即调用EventBus.getDefault().register()的一方)。

    ,通过EventBus的静态方法getDefault来获取一个实例,getDefault本身会调用其内部的构造方法,通过传入一个默认的EventBusBuilder来创建EventBus。

    获取了订阅者的class对象,然后SubscriberMethodFinder会找出所有订阅的方法。
    //首先从缓存中取出subscriberMethodss,如果有则直接返回该已取得的方法
    通过上面的方法把订阅方法找出来了,并保存在集合中,finduserinfo findState

    unregister
    //遍历所有订阅的事件
    调用了EventBus#unsubscribeByEventType,把订阅者以及事件作为参数传递了进去,那么应该是解除两者的联系。

    post
    通过currentPostingThreadState.get()来获取PostingThreadState,currentPostingThreadState是一个ThreadLocal,而ThreadLocal是每个线程独享的,其数据别的线程是不能访问的,因此是线程安全的。我们再次回到Post()方法,继续往下走,是一个while循环,这里不断地从队列中取出事件,并且分发出去,调用的是EventBus#postSingleEvent方法。

    线程切换过程(Scheduler)

    RxJava最好用的特点就是提供了方便的线程切换,被观察者切换线程使用observeOn(),观察者切换线程使用subscriberOn(),可切换为多种线程,但它的原理归根结底还是lift,使用subscribeOn()的原理就是创建一个新的Observable,把它的call过程开始的执行投递到需要的线程中;而 observeOn() 则是把线程切换的逻辑放在自己创建的Subscriber中来执行。把对于最终的Subscriber1的执行过程投递到需要的线程中来进行。

    subscribeOn()的线程切换发生在 OnSubscribe 中,即在它通知上一级 OnSubscribe 时,这时事件还没有开始发送,因此 subscribeOn() 的线程控制可以从事件发出的开端就造成影响;而 observeOn() 的线程切换则发生在它内建的 Subscriber 中,即发生在它即将给下一级 Subscriber 发送事件时,因此 observeOn() 控制的是它后面的线程。


    为什么 subscribeOn()只有第一个有效?
    举个例子,subscribeOn2从通知开始将后面的执行全部投递到需要的线程2来执行,但是之后的投递会受到在subscribeOn2的上级subscribeOn1的的影响,subscribeOn1又会把执行投递到线程1中去,这样执行就不受到subscribeOn2的控制了。所以只有第一个subscribeOn有效。

    什么是Retrofit

    Retrofit是针对于Android/Java的、基于okHttp的、一种轻量级且安全的、Retrofit采用注解方式开发。通过注解构建不同的请求和请求的参数,
    首先通过构造者模式获取Retrofit对象,然后通过动态代理获取到所定义的接口实例,得到实例后就可以调用接口的方法来获取Call对象最后进行网络请求操作
    调用call对象的enqueue方法,并且传入Callback参数,实现onResponse方法和onFailure方法,表示请求成功和失败时候的回调。

    image.png

    获得了这个call对象就可以开始访问网络了。Call接口提供enqueue(Callback callback)方法,其中泛型T即Call的泛型,在这里就是ResponseBody。Callback接口包含两个方法,OnResponse和onFailure,分别在请求成功和失败的时候回调。于是call调用的形式如下:

    ButterKnife 原理解析

    原理解析
    首先得到要绑定的 Activity 对应的 Class,然后用根据 Class 得到一个继承了Unbinder的Constructor,最后通过反射constructor.newInstance(target, source)得到Unbinder子类的一个实例,
    BINDINGS是一个LinkedHashMap:
    缓存了对应的 Class 和 Constructor 以提高效率!
    所以最终bind()方法返回的是MainActivity_ViewBinding类的实例
    核心就是把findRequiredView()得到的 View 转成指定类型的 View ,如果 xml 中定义的 View 和 Activity 中通过注解绑定的 View 类型不一致,就会抛出上边方法的异常

    apk的打包过程分7步:

    打包流程
    1、打包资源文件,生成R.java文件
    2、处理aidl文件,生成相应java 文件
    3、编译工程源代码,生成相应class 文件
    4、转换所有class文件,生成classes.dex文件
    5、打包生成apk6、对apk文件进行签名
    7、对签名后的apk文件进行对齐处理

    Android 异步消息处理机制 让你深入理解 Looper、Handler、Message三者关系

    三者关系
    1、首先Looper.prepare()在本线程中保存一个Looper实例,然后该实例中保存一个MessageQueue对象;因为Looper.prepare()在一个线程中只能调用一次,所以MessageQueue在一个线程中只会存在一个。

    2、Looper.loop()会让当前线程进入一个无限循环,不端从MessageQueue的实例中读取消息,然后回调msg.target.dispatchMessage(msg)方法。

    3、Handler的构造方法,会首先得到当前线程中保存的Looper实例,进而与Looper实例中的MessageQueue想关联。

    4、Handler的sendMessage方法,会给msg的target赋值为handler自身,然后加入MessageQueue中。

    5、在构造Handler实例时,我们会重写handleMessage方法,也就是msg.target.dispatchMessage(msg)最终调用的方法。

    好了,总结完成,大家可能还会问,那么在Activity中,我们并没有显示的调用Looper.prepare()和Looper.loop()方法,为啥Handler可以成功创建呢,这是因为在Activity的启动代码中,已经在当前UI线程调用了Looper.prepare()和Looper.loop()方法。

    主线程中的Looper.loop()一直无限循环为什么不会造成ANR?

    造成ANR的原因一般有两种:

    • 当前的事件没有机会得到处理(即主线程正在处理前一个事件,没有及时的完成或者looper被某种原因阻塞住了)
    • 当前的事件正在处理,但没有及时完成

    ActivityThread的main方法主要就是做消息循环,一旦退出消息循环,那么你的应用也就退出了。因为Android 的是由事件驱动的,looper.loop() 不断地接收事件、处理事件,每一个点击触摸或者说Activity的生命周期都是运行在 Looper.loop() 的控制之下,如果它停止了,应用也就停止了。只能是某一个消息或者说对消息的处理阻塞了 Looper.loop(),而不是 Looper.loop() 阻塞它。
    总结:Looer.loop()方法可能会引起主线程的阻塞,但只要它的消息循环没有被阻塞,能一直处理事件就不会产生ANR异常。

    Android加密算法有多种多样,常见的有MD5、RSA、AES、3DES四种。

    Android加密
    MD5是不可逆的加密算法,也就是无法解密,主要用于客户端的用户密码加密。
    RSA算法在客户端使用公钥加密,在服务端使用私钥解密。这样一来,即使加密的公钥被泄露,没有私钥仍然无法解密。(注意:使用RSA加密之前必须在AndroidStudio的libs目录下导入bcprov-jdk的jar包)

    SurfaceView和View最本质的区别在于,

    surfaceView是在一个新起的单独线程中可以重新绘制画面. 而View必须在UI的主线程中更新画面。那么在UI的主线程中更新画面 可能会引发问题,比如你更新画面的时间过长,那么你的主UI线程会被你正在画的函数阻塞。那么将无法响应按键,触屏等消息。 当使用surfaceView 由于是在新的线程中更新画面所以不会阻塞你的UI主线程。但这也带来了另外一个问题,就是事件同步。比如你触屏了一下,你需要surfaceView中thread处理,一般就需要有一个event queue的设计来保存touch event,这会稍稍复杂一点,因为涉及到线程同步。

    Serilizeable和Parcelable的区别?

    Serializable使用IO读写存储在硬盘上,
    Serializable的作用是将数据对象存入字节流当中,在需要时重新生成对象,主要应用是利用外部存储设备保存对象状态,以及通过网络传输对象等。
    implements Serializable接口的的作用就是给对象打了一个标记,系统会自动将其序列化。

    实现Parcelable接口
    而Parcelable是直接在内存中读写,很明显内存的读写速度通常大于IO读写,所以在Android中通常优先选择Parcelable。
    a、在使用内存的时候,Parcelable比Serializable性能高,所以推荐使用Parcelable类。
    b、Serializable在序列化的时候会产生大量的临时变量,从而引起频繁的GC。

    使用Serilizeable序列化的时候,有一个序列化id,它的作用是什么?

    关于serialVersionUID最好显式定义,表明类的版本,即根据类型和成员变量生成一个64位Hash值
    反序列化时就可以根据这个id校验类版本

    文件断点续传的简单实现

    传输开始之前发送方先向接收方发送一个确认信息,然后再向接收方发送准备发送的文件的文件名
    接收方收到确认信息之后,接收从发送方发送过来的文件名,接收完之后向发送方发送一个确认信息表示文件名接收完毕,然后接收方根据收到的文件名创建一个“.temp”File对象和一个“.temp”RandomAccessFile对象。获取这个File对象所对应文件的长度(大小)(这个长度就是接收方已经接受的长度,如果之前没有接收过这个文件,长度就为0),并把文件长度发送给发送方。
    发送方收到确认信息之后,接收接受方发送的文件长度,然后向接收方发送准备发送的文件的总长度,并向接收方发送一个确认信息。然后根据接收方发送的文件长度,从文件对应长度的位置开始发送。
    接收方收到确认信息之后,接受发送方发送过来的数据,然后从此文件的末尾写入。接受完成之后再将“.temp”文件重命名为正常的文件名。

    image.png

    ok”表示确认信息
    能够实现断点续传的关键就是使用了RandomAccessFile,此类的实例支持对随机访问文件的读取和写入。

    TCP协议和UDP协议的区别。TCP协议和UDP协议在传输发生丢包时分别会做什么处理?

    TCP协议是面向连接的,UDP协议不面向连接。TCP协议可靠,报文更复杂,通信速度慢于UDP;UDP报文简单,速度比TCP快但不保证传输可靠。TCP通过检验和、序列号、确认应答、重发控制、窗口控制,连接管理保证通信的可靠性。

    HTTPS和HTTP的区别主要如下:

    1、https协议需要到ca申请证书,一般免费证书较少,因而需要一定费用。
      2、http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。
     3、http和https使用的是完全不同的连接方式,用的端口也不一样,前者是80,后者是443。
      4、http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。

    HTTPS 三次握手

    1:客户端发送了一个带有SYN的Tcp报文到服务器,这个三次握手中的开始。表示客户端想要和服务端建立连接。
    2:服务端接收到客户端的请求,返回客户端报文,这个报文带有SYN和ACK标志,询问客户端是否准备好。
    3:客户端再次响应服务端一个ACK,表示我已经准备好。

    四次挥手

    1、第一次挥手:客户端设置seq和ACK,向服务端发送一个FIN报文段.此时,客户端进入FIN_WAIT_1状态,表示客户端没有数据要发送给服务端了.
    2、第二次挥手:服务端收到报文段,向客户端返回一个ACK报文段
    3、第三次挥手:服务端向客户端发送FIN报文段,请求关闭连接,同时服务端改变状态,进入LAST_ACK状态
    4、第四次挥手:客户端收到服务端的报文段,向服务端发送ACK报文段,然后客户端进入TIME_WAIT状态.服务端收到报文段之后就连接关闭,此时客户端等待2msl(最大报文段生存时间)后依然没有收到回复,则说明服务端已经正常关闭,这样客户端也可以关闭连接了.

    一个HTTP报文由3部分组成,分别是:

    (1)、起始行(start line)
      (2)、首部(header)
      (3)、主体(body)

    为什么建立连接协议是三次握手,而关闭连接却是四次握手呢?

    这是因为服务端的LISTEN状态下的SOCKET当收到SYN报文的建连请求后,它可以把ACK和SYN(ACK起应答作用,而SYN起同步作用)放在一个报文里来发送。但关闭连接时,当收到对方的FIN报文通知时,它仅仅表示对方没有数据发送给你了;但未必你所有的数据都全部发送给对方了,所以你不可以马上关闭SOCKET,也即你可能还需要发送一些数据给对方之后,再发送FIN报文给对方来表示你同意现在可以关闭连接了,所以它这里的ACK报文和FIN报文多数情况下都是分开发送的。

    为什么TIME_WAIT状态还需要等2MSL后才能返回到CLOSED状态?

    这是因为虽然双方都同意关闭连接了,而且握手的4个报文也都协调和发送完毕,按理可以直接回到CLOSED状态(就好比从SYN_SEND状态到ESTABLISH状态那样);但是因为我们必须要假想网络是不可靠的,你无法保证你最后发送的ACK报文会一定被对方收到,因此对方处于LAST_ACK状态下的SOCKET可能会因为超时未收到ACK报文,而重发FIN报文,所以这个TIME_WAIT状态的作用就是用来重发可能丢失的ACK报文。

    Java通过ThreadpoolExecutors创建线程池,

    总共有四类
    newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程
    newFixedThreadPool创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待
    。newScheduledThreadPool创建一个定长线程池,支持定时和周期性任务执行
    newSingleThreadExecutor创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO,LIFO,优先级)执行。

    i++和++i的线程安全分为两种情况:

    1、如果i是局部变量(在方法里定义的),那么是线程安全的。因为局部变量是线程私有的,别的线程访问不到,其实也可以说没有线程安不安全之说,因为别的线程对他造不成影响。

    2、如果i是全局变量(类的成员变量),那么是线程不安全的。因为如果是全局变量的话,同一进程中的不同线程都有可能访问到。

    平衡二叉树,

    它的左子树和右子树都是平衡二叉树,且左子树和右子树的深度之差的绝对值不超过1

    5.0到9.0 新特性

    Android 5.0(Android Lollipop)开始,android迎来了扁平化时代,使用一种新的Material Design 设计风格,设计了全新的通知中心,
    Android 6.0引入了动态权限管理,指纹识别
    7.0分屏多任务,新通知消息,通知消息快捷回复,夜间模式
    8.0 画中画功能, 在Android 8.0“奥利奥”中,应用图标的右上角有一个小点,它代表未读通知,通知延时提醒,自动填充
    9.0 刘海设计,黑白模式切换,加入长截图,允许定制主屏搜索栏,应用多开

    相关文章

      网友评论

        本文标题:Android进阶知识点

        本文链接:https://www.haomeiwen.com/subject/mhzwrqtx.html