移动端面试及资料整理

71 阅读1小时+

Mac使用

讲Mac的软件环境备份,方案更换设备的时候一件克隆-应用。

对应简历的面试问题

Android面试总结

可以看看的Android面试总结

  1. Android面试总结 1
  2. Kotlin面试题
  3. Flutter面试题
  4. Android知识点总结

简历对应的面试部分

自我介绍

自我介绍 我2017年毕业,***

面试问题部分

面试问题

埋点方案

APP灰度方案

面试的问题:

Android高频算法面试

Android高频算法面试

Android ANR 分析

Android 高频算法题汇总

  1. Java/Kotlin基础知识,Android基础知识,常问的面试题
  2. 组件化架构如何划分
  3. 组件间如何通信?ARouter实现原理,有什么缺点,如何解决?
  4. 组件的库如何初始化,如何保证组件之间的初始化顺序
  5. MVC、MVP、MVVM、MVI架构的实现和区别
  6. APK包瘦身
  7. APP应用冷启动,Activity冷启动的过程
  8. APP内存优化,Bitmap的内存优化,内存泄漏的治理
  9. APP网络优化,OkHttp的原理,连接池原理,线程池的调度原理,线程的创建过程,Socket三次握手简历连接,Socket四次挥手断开连接,发送的ack数据包以及参数的左右和每次握手(或者挥手)当前所处的状态,Retrofit的原理。网络协议模型。Http和Https的区别。TCP和UDP的区别。
  10. EventBus的原理
  11. H5秒开优化,原生和JS之间的相互通信,富文本编辑器的开发
  12. APP的卡顿优化,卡顿发生的原因,原因采集,卡顿治理,卡顿监控
  13. IM即时通讯功能的集成开发,表的设计,数据存储,数据同步
  14. APP的打包编译过程,Gradle的执行构建、Task的顺序,CI/CD打包流水线构建
  15. 视频播放器集成开发,视频秒播优化、视频数据格式,实现类似抖音的上下滑动视频播放效果,RecyclerView的四级缓存,YUV和NV21的数据格式
  16. 社区业务图片和视频的处理
  17. 跨进程通信的方式,Binder通信机制
  18. Flutter跨平台开发,Flutter与原生之间的通信方式
  19. 刷一刷常见的算法题
  20. JNI的编写过程
  21. NDK开发的过程
  22. 串口通信的实现
  23. 蓝牙通信开发
  24. USB通信开发

1. Java/Kotlin基础知识,Android基础知识,常问的面试题

Java基础知识
垃圾回收
GC Roots对象
  1. Java 虚拟机栈中的对象
  2. 本地方法栈中的对象
  3. 方法区中的静态对象
  4. 常量池中的常量
  • 引用计数法: 当一个对象被引用时,它的引用计数器会加一,垃圾回收时会清理掉引用计数为0的对象。但这种方法有一个问题,比方说有两个对象 A 和 B,A 引用了 B,B 又引用了 A,除此之外没有别的对象引用 A 和 B,那么 A 和 B 在我们看来已经是垃圾对象,需要被回收,但它们的引用计数不为 0,没有达到回收的条件。正因为这个循环引用的问题,Java 并没有采用引用计数法。
  • 可达性分析法: 我们把 Java 中对象引用的关系看做一张图,从根级对象不可达的对象会被垃圾收集器清除。根级对象一般包括 Java 虚拟机栈中的对象、本地方法栈中的对象、方法区中的静态对象和常量池中的常量。 回收垃圾的话有这么四种方法:
  • 标记清除算法: 顾名思义分为两步,标记和清除。首先标记到需要回收的垃圾对象,然后回收掉这些垃圾对象。标记清除算法的缺点是清除垃圾对象后会造成内存的碎片化。
  • 复制算法: 复制算法是将存活的对象复制到另一块内存区域中,并做相应的内存整理工作。复制算法的优点是可以避免内存碎片化,缺点也显而易见,它需要两倍的内存。
  • 标记整理算法: 标记整理算法也是分两步,先标记后整理。它会标记需要回收的垃圾对象,清除掉垃圾对象后会将存活的对象压缩,避免了内存的碎片化。
  • 分代算法: 分代算法将对象分为新生代和老年代对象。那么为什么做这样的区分呢?主要是在Java运行中会产生大量对象,这些对象的生命周期会有很大的不同,有的生命周期很长,有的甚至使用一次之后就不再使用。所以针对不同生命周期的对象采用不同的回收策略,这样可以提高GC的效率。

新生代对象分为三个区域:Eden 区和两个 Survivor 区。新创建的对象都放在 Eden区,当 Eden 区的内存达到阈值之后会触发 Minor GC,这时会将存活的对象复制到一个 Survivor 区中,这些存活对象的生命存活计数会加一。这时 Eden 区会闲置,当再一次达到阈值触发 Minor GC 时,会将Eden区和之前一个 Survivor 区中存活的对象复制到另一个 Survivor 区中,采用的是我之前提到的复制算法,同时它们的生命存活计数也会加一。

这个过程会持续很多遍,直到对象的存活计数达到一定的阈值后会触发一个叫做晋升的现象:新生代的这个对象会被放置到老年代中。 老年代中的对象都是经过多次 GC 依然存活的生命周期很长的 Java 对象。当老年代的内存达到阈值后会触发 Major GC,采用的是标记整理算法。

JVM内存区域的划分,哪些区域会发生 OOM

JVM 的内存区域可以分为两类:线程私有的区域和线程共有的区域。 线程私有的区域:程序计数器、JVM 虚拟机栈、本地方法栈 线程共有的区域:堆、方法区、运行时常量池

  • 程序计数器。 每个线程有有一个私有的程序计数器,任何时间一个线程都只会有一个方法正在执行,也就是所谓的当前方法。程序计数器存放的就是这个当前方法的JVM指令地址。
  • JVM虚拟机栈。 创建线程的时候会创建线程内的虚拟机栈,栈中存放着一个个的栈帧,对应着一个个方法的调用。JVM 虚拟机栈有两种操作,分别是压栈和出栈。栈帧中存放着局部变量表、方法返回值和方法的正常或异常退出的定义等等。
  • 本地方法栈。 跟 JVM 虚拟机栈比较类似,只不过它支持的是 Native 方法。
  • 堆。 堆是内存管理的核心区域,用来存放对象实例。几乎所有创建的对象实例都会直接分配到堆上。所以堆也是垃圾回收的主要区域,垃圾收集器会对堆有着更细的划分,最常见的就是把堆划分为新生代和老年代。
  • 方法区。 方法区主要存放类的结构信息,比如静态属性和方法等等。
  • 运行时常量池。 运行时常量池位于方法区中,主要存放各种常量信息。

其实除了程序计数器,其他的部分都会发生 OOM。

  • 堆。 通常发生的 OOM 都会发生在堆中,最常见的可能导致 OOM 的原因就是内存泄漏。

  • JVM虚拟机栈和本地方法栈。 当我们写一个递归方法,这个递归方法没有循环终止条件,最终会导致 StackOverflow 的错误。当然,如果栈空间扩展失败,也是会发生 OOM 的。

  • 方法区。 方法区现在基本上不太会发生 OOM,但在早期内存中加载的类信息过多的情况下也是会发生 OOM 的。

Java的内存模型

Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。

  • 原子性

在Java中,为了保证原子性,提供了两个高级的字节码指令monitorentermonitorexit。在synchronized的实现原理文章中,介绍过,这两个字节码,在Java中对应的关键字就是synchronized

因此,在Java中可以使用synchronized来保证方法和代码块内的操作是原子性的。

  • 可见性

Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值的这种依赖主内存作为传递媒介的方式来实现的。

Java中的volatile关键字提供了一个功能,那就是被其修饰的变量在被修改后可以立即同步到主内存,被其修饰的变量在每次是用之前都从主内存刷新。因此,可以使用volatile来保证多线程操作时变量的可见性。

除了volatile,Java中的synchronizedfinal两个关键字也可以实现可见性。只不过实现方式不同,这里不再展开了。

  • 有序性

在Java中,可以使用synchronizedvolatile来保证多线程之间操作的有序性。实现方式有所区别:

volatile关键字会禁止指令重排。synchronized关键字保证同一时刻只允许一条线程操作。

好了,这里简单的介绍完了Java并发编程中解决原子性、可见性以及有序性可以使用的关键字。读者可能发现了,好像synchronized关键字是万能的,他可以同时满足以上三种特性,这其实也是很多人滥用synchronized的原因。

但是synchronized是比较影响性能的,虽然编译器提供了很多锁优化技术,但是也不建议过度使用。

类加载过程

Java 中类加载分为 3 个步骤:加载、链接、初始化。

  • 加载。 加载是将字节码数据从不同的数据源读取到JVM内存,并映射为 JVM 认可的数据结构,也就是 Class 对象的过程。数据源可以是 Jar 文件、Class 文件等等。如果数据的格式并不是 ClassFile 的结构,则会报 ClassFormatError。
  • 链接。 链接是类加载的核心部分,这一步分为 3 个步骤:验证、准备、解析。
1. **验证。** 验证是保证JVM安全的重要步骤。JVM需要校验字节信息是否符合规范,避免恶意信息和不规范数据危害JVM运行安全。如果验证出错,则会报VerifyError。
2.  **准备。** 这一步会创建静态变量,并为静态变量开辟内存空间。
3.  **解析。** 这一步会将符号引用替换为直接引用。
  • 初始化。 初始化会为静态变量赋值,并执行静态代码块中的逻辑。
双亲委派模型

类加载器大致分为3类:启动类加载器、扩展类加载器、应用程序类加载器。

  • 启动类加载器主要加载 jre/lib下的jar文件。
  • 扩展类加载器主要加载 jre/lib/ext 下的jar文件。
  • 应用程序类加载器主要加载 classpath 下的文件。

所谓的双亲委派模型就是当加载一个类时,会优先使用父类加载器加载,当父类加载器无法加载时才会使用子类加载器去加载。这么做的目的是为了避免类的重复加载。

Kotlin基础知识

Kotlin基础知识+面试

Android基础知识
Activity启动模式

standard:标准模式:如果在mainfest中不设置就默认standard;standard就是新建一个Activity就在栈中新建一个activity实例;

singleTop:栈顶复用模式:与standard相比栈顶复用可以有效减少activity重复创建对资源的消耗,但是这要根据具体情况而定,不能一概而论;

singleTask:栈内单例模式,栈内只有一个activity实例,栈内已存activity实例,在其他activity中start这个activity,Android直接把这个实例上面其他activity实例踢出栈GC掉;

singleInstance :堆内单例:整个手机操作系统里面只有一个实例存在就是内存单例;

在singleTop、singleTask、singleInstance 中如果在应用内存在Activity实例,并且再次发生startActivity(Intent intent)回到Activity后,由于并不是重新创建Activity而是复用栈中的实例,因此Activity再获取焦点后并没调用onCreate、onStart,而是直接调用了onNewIntent(Intent intent)函数;

LauchMode Instance

standard 邮件、mainfest中没有配置就默认标准模式

singleTop 登录页面、WXPayEntryActivity、WXEntryActivity 、推送通知栏

singleTask 程序模块逻辑入口:主页面(Fragment的containerActivity)、WebView页面、扫一扫页面、电商中:购物界面,确认订单界面,付款界面

singleInstance 系统Launcher、锁屏键、来电显示等系统应用

activity横竖屏切换时activity的生命周期,view的生命周期
  • 不配置configChanges时:切换横竖屏时生命周期各自都会走一遍
  • 配置configChanges时:必须设置为android:configChanges="orientation|screenSize"时,才不会重走生命周期方法,只会回调onConfigurationChanged方法,注意,不配置configChanges或是配置了但不同时包含这两个值时,都会重走一遍生命周期方法,并且不会回调onConfigurationChanged方法。
  • 另外重走生命周期方法时,还会调用onSaveInstanceState() onRestoreIntanceState() ,资源相关的系统配置发生改变或者资源不足:例如屏幕旋转,当前Activity会销毁,并且在onStop之前回调onSaveInstanceState保存数据,在重新创建Activity的时候在onStart之后回调onRestoreInstanceState。其中Bundle数据会传到onCreate(不一定有数据)和onRestoreInstanceState(一定有数据)。用户或者程序员主动去销毁一个Activity的时候不会回调,其他情况都会调用,来保存界面信息。如代码中finish()或用户按下back,不会回调。
Activity的冷启动流程

image.png

Launcher进程通过ActivityManagerProxy跨进程发送startActivity的请求到ActivityManagerService,AMS通过socket跨进程请求Zygote fork处目标应用进程(client进程),应用进程通过ActivityManagerProxy发送attachApplication到ActivityManagerService,AMS通过ApplicationThreadProxy发送scheduleLaunchActivity到App应用进程,ApplicationThread通过Handler将launchActivity切换到主线程委托给Instrumentaion执行,后续Activity的生命周期调用类似的重复以上跨进程通信交互。

Activity启动

Service的生命周期
Service的启动方式
Android屏幕刷新机制

利用Choreographer监控应用的帧率。

Android屏幕刷新机制详解,Systrace

View的绘制流程

代码的关键调用:

--> WindowManagerImpl.addView(decorView, layoutParams) 
--> WindowManagerGlobal.addView()
WindowManagerGlobal.addView()负责创建ViewRootImpl,同时执行root.setView(view, wparams, panelParentView);

ViewRootImpl.setView(decorView, layoutParams, parentView)
-->ViewRootImpl.requestLayout()
-->scheduleTraversals()
-->TraversalRunnable.run()
-->doTraversal()
-->performTraversals()(performMeasure、performLayout、performDraw)

View的绘制从ActivityThread的Handler处理RESUME_ACTIVITY事件之后,在执行performResumeActivity之后,创建Window和DecorView并调用WindowManager的addView方法,在addView方法中会调用ViewRootImpl的setView方法,最终执行performTraversals方法,依次执行performMeasure、performLayout、performDraw。

分别对应View绘制的三大流程。在performMeasure中会依次调用measure、onMeasure,测量view的大小,最后调用setMeasuredDimension设置测量的结果。如果是ViewGroup,会遍历子View测量自身大小并设置结果。

在performLayout中会依次调用layout、onLayout,根据measure阶段测量的大小确定view在屏幕中的位置。该阶段只在ViewGroup中执行。

在performDraw中,会依次调用draw、onDraw,在onDraw()方法中首先会绘制背景,然后绘制view的内容,再然后调用dispatchDraw()调用子view的draw方法,最后绘制滚动条。ViewGroup默认不会执行onDraw方法,如果复写了onDraw(Canvas)方法,需要调用 setWillNotDraw(false);清除不需要绘制的标记。

resuestLayout、invalidate和postInvalidate有什么区别?

  • requestLayout: 该方法会递归调用父窗口的requestLayout,最终触发ViewRootImpl的performTraversals方法,此时mLayoutRequested为true,会触发onMeasure和onLayout,但不一定会触发onDraw。

  • invalidate: 该方法递归调用父View的invalidateChildInParent()方法,直到调用ViewRootImpl的invalidateChildInParent()方法,最终触发ViewRootImpl的performTraversals()方法,此时mLayoutRequestede为false,不会触发onMesaure()与onLayout()方法,但是会触发onDraw()方法。

  • postInvalidate: 该方法跟invalidate流程一样,只不过可以在子线程中调用。

事件分发机制

用户对屏幕的操作的事件可以划分为3种最基础的事件:
1.ACTION_DOWN:手指刚接触屏幕,按下去的那一瞬间产生该事件
2.ACTION_MOVE:手指在屏幕上移动时候产生该事件
3.ACTION_UP:手指从屏幕上松开的瞬间产生该事件

Android 的事件分发机制大体可以分为三部分 事件生产 事件分发 事件消费

事件分发的大概流程可以这样来描述:Activity -> PhoneWindow ->DecorView(DecorView其实就是一种ViewGroup) ->View,分发传递。

事件分发需要的三个重要方法来共同完成:

public boolean dispatchTouchEvent(event):用于进行点击事件的分发

public boolean onInterceptTouchEvent(event):用于进行点击事件的拦截

public boolean onTouchEvent(event):用于处理点击事件

image.png

事件继续分发需要调用super对应的方法。

事件分发详解

  1. 事件类型:

    • Android 中的事件包括触摸事件(Touch Events)、按键事件(Key Events)、轨迹球事件(Trackball Events)等。
    • 主要关注触摸事件,它包括按下、移动、抬起等不同的阶段。
  2. View层次结构:

    • Android 应用的 UI 是一个 View 层次结构,由多个 View 组成,每个 View 都可以处理事件。
    • View 的分层结构决定了事件的传递顺序。
  3. 事件分发:

    • 事件首先从最上层的父容器传递到最底层的子 View。
    • 在每个阶段,父容器或 View 都有机会拦截、处理或传递事件。
    • dispatchTouchEvent 方法是事件分发的入口,它负责将事件分发给 View 层次结构中的相应节点。
  4. 事件拦截:

    • 在事件分发的过程中,父容器和子 View 都有机会拦截事件。
    • 如果某个 View 返回 true,表示它已经完全处理了该事件,事件不再传递给其他 View。
    • 如果某个 View 返回 false 或调用 super.dispatchTouchEvent(),事件继续向下传递。
  5. 事件处理:

    • 如果某个 View 的 onTouchEvent 方法返回 true,表示该 View 已经处理了该事件。
    • 如果返回 false,则表示该 View 不处理该事件,事件会传递到其父容器。
  6. 点击事件处理:

    • 当一个点击事件(如 ACTION_UP)发生时,最后处理事件的那个 View 会触发 onClick 方法,如果设置了点击监听器。
  7. 事件流程总结:

    • ActivitydispatchTouchEvent 方法首先接收到事件。
    • 事件经过 ActivityPhoneWindowDecorViewViewGroup 层次结构的各个节点。
    • 每个节点都有机会拦截、处理或传递事件。
    • 事件最终到达包含目标 View 的节点,由该 View 的 dispatchTouchEvent 方法处理。
插件式换肤

布局的加载解析过程 (插件化换肤) 前提:待换肤的apk资源id与换肤包apk中的资源id要相等。

原理:利用apk包解析、资源加载和xml布局解析创建view并设置资源,来完成插件式换肤。

apk包资源的解析: 通过AssetManager.addAssetPath()加载apk包,在jni层去解析apk包,构造处Resource对象,通过名称、类型、包名获取资源。

// 获取要换肤的资源id:
try {
    int attributeCount = attrs.getAttributeCount();
    for (int i = 0; i < attributeCount; i++) {
        String attributeValue = attrs.getAttributeValue(i);
        if (attributeValue.startsWith("@")) {
            String attrValue = attributeValue.substring(1);
            int resId = Integer.parseInt(attrValue);
            // 资源名称
            String resourceName = context.getResources().getResourceEntryName(resId);
            // 资源类型
            String resourceType = context.getResources().getResourceTypeName(resId);
            // 获取资源皮肤包中对应的资源id
           int skinResId = skinResources.getIdentifier(resourceName, resourceType, "换肤apk的包名");
        }
    }
} catch (Exception e) {
    e.printStackTrace();
}

xml加载解析: setContentView,调用phoneWindow.setContentView(),新建DecorView,然后加载系统R.id.content这个布局作为DecorView的根布局,然后将setContentView的布局添加到contentView中。在LayoutInflater加载xml布局,使用XmlPullParser解析xml布局,最终经过LayoutInflater.Factory的实现类通过反射创建view。

try {
            Resources superRes = getResources();
            // 创建AssetManager,但是不能直接new所以只能通过反射
            AssetManager assetManager = AssetManager.class.newInstance();
            // 反射获取addAssetPath方法
            Method addAssetPathMethod = AssetManager.class.getDeclaredMethod("addAssetPath",String.class);
            // 皮肤包的路径:  本地sdcard/plugin.skin
            String skinPath = Environment.getExternalStorageDirectory().getAbsoluteFile()+ File.separator+"plugin.skin";
            // 反射调用addAssetPath方法
            addAssetPathMethod.invoke(assetManager, skinPath);
            // 创建皮肤的Resources对象
            Resources skinResources = new Resources(assetManager,superRes.getDisplayMetrics(),superRes.getConfiguration());
            // 通过资源名称,类型,包名获取Id
            int bgId = skinResources.getIdentifier("main_bg","drawable","com.hc.skin");
            Drawable bgDrawable = skinResources.getDrawable(bgId);
            // 设置背景
            findViewById(R.id.activity_main).setBackgroundDrawable(bgDrawable);
        } catch (Exception e) {
            e.printStackTrace();
        }
  1. 资源的获取:反射获取AssetManager对象,通过addAssetPath方法传入资源包路径加载皮肤资源,创建获取皮肤包中资源的Resources对象,通过资源名称、类型、包名获取资源。
  2. 获取待换肤的View:自定义LayoutInflater.Factory,通过反射覆盖系统的Factory,在onCreateView回调方法中拿到新建的view和view对应的AttributeSet属性,遍历AttributeSet找到要换肤的属性名称和属性值,连同view一起缓存到集合中。
  3. 执行换肤:遍历要换肤的view集合,根据view要换肤的属性名和属性值,通过第一步自定义皮肤Resources对象获取到资源包中对应的皮肤属性资源,然后应用到view的相应属性上。

概括:自定义LayoutInflater.Factory通过反射覆盖系统自身加载解析XML的LayoutInflater.Factory,目的是在解析创建view的时候能在LayoutInflater.Factory的onCreateView回调方法中通过参数view和AttributeSet拿到需要换肤的view和属性,然后包装对象缓存起来。触发换肤的时候,通过反射拿到AssetManager和Resource对象,根据资源名称、类型和包名加载apk皮肤包中的资源,遍历缓存的view和待换肤属性,重新赋值换肤的资源,调用invalidate刷新view。

插件式换肤原理

插件式换肤实践

跨进程通信方式
Binder实现原理

利用内核空间的缓冲区映射实现用户端进程之间的通信。

利用内存映射,Binder的map函数会将内核空间直接与用户空间对应,Binder驱动在Client进程和Server进程之间建立起一条通道,Server进程间接实现Binder,Client进程获取Binder的代理对象(实际是Binder引用),用户空间可以直接访问内核空间的数据,由此通过Binder实现跨进程通信。

Binder进程讲解

Linux跨进程通信方式:

  • 1)管道Pipe: 在内存中创建一个共享文件,利用共享文件传递信息。该共享文件并不是文件系统,只存在于内存中;只能在一个方向上流动
  • 2)信号Signal: 异步通信。信号在用户空间和内核空间之间交互,内核可利用信号来通知用户空间的进程发生哪些系统事件。不适用于信号交换,适用于过程中断控制;
  • 3)信号量Semaphore: 控制多个进程对共享资源的访问。主要是进程间以及同一进程不同线程之间的同步手段;
  • 4)消息队列 Message Queue: 存放在内存中并由消息对了标识符标识,允许一个或多个进程对它进行读写消息。信息会复制两次,不适用于频繁或信息量大的通信
  • 5)共享内存Shared Memory: 直接读写内核的一块内存空间。不需要进行数据拷贝
  • 6)套接字Socket: 不同机器之间进程间通信。

Android跨进程通信方式:

  • 1)Bundle: 实现了Parcelable接口,常用于Activity、Service、BroadcastReceive之间的通信
  • 2)文件共享: 常用于无并发,交换实时性不高的数据
  • 3)Messenger: 低并发的一对多即时通信。串行的方式处理Client发来的消息,只能传输数据,不能方法调用(RPC)
  • 4)ContentProvider: 存储和获取数据,不同程序之间共享数据。一对多的数据共享
  • 5)AIDL
  • 6)Socket: 网络数据交换
AIDL的实现过程

AIDL详解

定向tag: AIDL中的定向 tag 表示了在跨进程通信中数据的流向,其中 in 表示数据只能由客户端流向服务端, out 表示数据只能由服务端流向客户端,而 inout 则表示数据可在服务端与客户端之间双向流通。其中,数据流向是针对在客户端中的那个传入方法的对象而言的。in 为定向 tag 的话表现为服务端将会接收到一个那个对象的完整数据,但是客户端的那个对象不会因为服务端对传参的修改而发生变动;out 的话表现为服务端将会接收到那个对象的的空对象,但是在服务端对接收到的空对象有任何修改之后客户端将会同步变动;inout 为定向 tag 的情况下,服务端将会接收到客户端传来对象的完整信息,并且客户端将会同步服务端对该对象的任何变动。

Fragment的生命周期

image.png

Fragment的切换详解

ThreadLocal

在介绍Handler消息机制之前有必要了解下ThreadLocal,ThreadLocal的作用是用来隔离多线程环境下数据的影响,保证数据在当前线程环境下不会被污染。

原理: 每一个Thread都持有一个ThreadLocalMap用来存放数据,不同线程持有的ThreadLocalMap内存空间都是不一样的,从空间隔离的维度保证数据在多线程环境下的安全。

Handler的通信原理

Handler: 处理Message。

Looper: 从队列中取出消息。

MessageQueue: 存放Message的时间有序队列。

Message: 承载消息数据。

主流程: Looper的私有构造方法中新建了ThreadLocal和MessageQueue,在调用prepareLooper初始化Looper时通过ThreadLocal建立了Looper与当前线程一一对应的关系,Looper持有MessageQueue,因此也建立了一一对应的关系。 Handler调用sendMessage的时候,获取到线程的Looper和MessageQueue,然后执行消息入队列。 Looper.loop开启循环,通过messageQueue不断通过next获取队列的消息,然后通过handler.dispatchMessage执行消息的分发。

Handler如何完成跨线程通信的? 在孵化进程的时候会给每一个进程分配一个地址空间,同一个进程中的线程共享该进程的堆内存空间,Handler实例分配在堆空间上,在线程间是共享的,所以同一个进程中可以依赖handler完成线程间的通信。

线程切换:handler所在的线程由Looper所在的线程决定,handler跟looper是绑定的,Looper跟当前线程是绑定的,Looper.loop()消息循环,在哪个线程决定了handler.handleMessage()在哪个线程处理消息。

MessageQueue阻塞的情况:

  • 延迟消息:下一条消息执行时间还未到的时候
  • 消息队列为空
  • 队列中存在同步屏障,没有异步消息的时候

MessageQueue唤醒的情况:

  • 设置阻塞延迟的时间到了,执行自动唤醒
  • 当队列里边插入新的消息
  • 队列中存在同步屏障,插入新的一步消息的时候

延迟阻塞消息执行的过程是什么样的?

如果头部的这个Message是有延迟而且延迟时间没到的(now < msg.when),会计算一下时间(保存为变量nextPollTimeoutMillis),然后在循环开始的时候判断如果这个Message有延迟,就调用nativePollOnce(ptr, nextPollTimeoutMillis)进行阻塞。

  1. postDelay()一个10秒钟的Runnable A、消息进队,MessageQueue调用nativePollOnce()阻塞,Looper阻塞;
  2. 紧接着post()一个Runnable B、消息进队,判断现在A时间还没到、正在阻塞,把B插入消息队列的头部(A的前面),然后调用nativeWake()方法唤醒线程;
  3. MessageQueue.next()方法被唤醒后,重新开始读取消息链表,第一个消息B无延时,直接返回给Looper;
  4. Looper处理完这个消息再次调用next()方法,MessageQueue继续读取消息链表,第二个消息A还没到时间,计算一下剩余时间(假如还剩9秒)继续调用nativePollOnce()阻塞;直到阻塞时间到或者下一次有Message进队。

在Meaasge入队的时候会对消息的时间进行排序计算,保证message是按照时间顺序插入的。

同步屏障 MessageQueue.postSyncBarrier()

view.post()和handler.post()有什么区别

view.post()添加一个runnable任务到ViewRootImpl中进行缓存,在屏幕刷新Chorographer.postCallback添加一个mTraversalRunnable异步消息任务到handler消息队列中,执行异步消息进入到view的绘制流程,在performTraversals中会调用之前缓存在ViewRootImpl中的runnable添加到handler消息队列执行,当前view的绘制执行完成之后才会执行view.post的runnable,所以view.post runnable的中回调中能够拿到view的测量尺寸。

RecyclerView

DiffUtils的讲解
RecyclerView的四级缓存:

  • 屏幕内部缓存:缓存没有与RecyclerView分离的ViewHolder和数据已经发生改变的ViewHolder(这是一级缓存)
// 一级缓存中用来存储屏幕中显示的ViewHolder
final ArrayList<ViewHolder> mAttachedScrap = new ArrayList<>();
// 数据发生改变的缓存viewHolder      
ArrayList<ViewHolder> mChangedScrap = null;
  • 二级缓存:用来缓存最近回收的ViewHolder,缓存复用时必须匹配position,这个集合里存的 ViewHolder 的原本数据信息都在,所以可以直接添加到 RecyclerView 中显示,不需要再次重新onBindViewHolder()。默认大小是2,最大的缓存大小默认为2个ViewHolder,可以通过Recycleview的setViewCacheSize方法进行设置缓存大小。
// 二级缓存中用来存储屏幕外的缓存
final ArrayList<ViewHolder> mCachedViews = new ArrayList<ViewHolder>();
  • 三级缓存:ViewCacheExtension给用户自定义实现的缓存,需要实现方法:
 public abstract View getViewForPositionAndType(Recycler recycler, int position, int type);
  • 四级缓存:RecyclerViewPool缓存已经跟RecyclerView分离的ViewHolder,获取的时候判断viewType,不做校验但是要重新设置数据调用onBindViewHolder方法。
APP的编译打包原理
Android模块源码的编译顺序
Android SDK不同版本之间的差异
获取采样率缩放图片(优化图片加载内存)

设置 BitmapFactory.Options 的 inJustDecodeBounds 为true(此时不会加载图片,会对图片进行解析)

从 BitmapFactory.Options 获取图片的原始宽高信息,对应 outWidth 和 outHeight

结合所需要的图片大小计算采样率 inSampleSize

设置 BitmapFactory.Options 的inJustDecodeBounds 为false,重新加载图片

LRUCache缓存队列的实现

LruCache与DisLruCache
Lru(Least Recently Used) 即最近最少使用算法,当缓存满时,会优先淘汰那些近期最少使用的缓存对象。(基于LinkedHashMap实现,对头放置最久未被使用的数据,队尾放置最近使用的数据,当缓存满时,从队头开始删除数据)
采用Lru算法的缓存有两种:LruCache 和 DiskLruCache,LruCache用于实现内存缓存,DiskLruCache用于实现存储设备缓存。

项目中的缓存使用:  在一些项目中通常会结合使用内存缓存和存储设备缓存,比如第一次从网络中获取到图片,将图片缓存到存储设备缓存和内存缓存,当再一次加载图片时,会首先从内存缓存获取查找需要的图片是否存在(从内存缓存获取图片的速度最快),如果没有,会到存储设备缓存获取查找需要的图片是否存在,如果没有,最后才再一次从网络获取图片。

LruCache:  内部采用 LinkedHashMap 以强引用的方式存储外界的缓存对象,提供 get 和 put 方法完成缓存的获取和添加。

  • 强引用:直接的对象引用。
  • 软引用:当一个对象只有软引用存在时,系统内存不足时此对象才会被gc回收。
  • 弱引用:当一个对象只有弱引用存在时,此对象会随时被gc回收。

使用LruCache实现内存缓存

private LruCache<String, Bitmap> mMemoryCache;

//设置缓存大小并计算bitmap大小
int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
    @Override
    protected int sizeOf(String key, Bitmap bitmap) {
        return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
    }
};

//获取缓存对象
mMemoryCache.get(key);

//添加缓存对象
mMemoryCache.put(key, bitmap);
DisLruCache

DiskLruCache 的创建通过 open(File directory, int appVersion, int valueCount, long maxSize) 方法获取对象。

  • directory: 缓存目录路径。如果希望应用卸载后删除缓存文件,就选择sd卡上的缓存目录;如果要保留则选择sd卡上的其他特定目录。
  • appVersion: 设置成1即可。当版本号改变时,之前的缓存文件会被清空,但大多数时候版本号更改缓存文件仍然有效。
  • valueCount: 表示单个节点所对应的数据的个数,一般设为1即可。
  • maxSize: 缓存的总容量。
private DiskLruCache mDiskLruCache;
private static final long DISK_CACHE_SIZE = 1024 * 1024 * 50;//50MB

File diskCacheDir = getDiskCacheDir(mContext, "bitmap");
if (!diskCacheDir.exists) {
    diskCacheDir.mkdirs();
}
mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1, DISK_CACHE_SIZE);
DiskLruCache的缓存添加

详细操作步骤:

  1. 通过图片的url将其转换成key(该key一般为md5,转为md5是防止url有特殊符号),通过key获取到Editor对象
  2. 通过Editor对象获取文件输出流,将图片写入缓存文件中
  3. 缓存完成后调用editor.commit()方法提交才正式写入为缓存,如果写入期间出现异常,调用editor.abort()回滚操作。
private static final int DISK_CACHE_INDEX = 0; //因为在DiskLruCache创建时open()方法中的valueCount设置为1,这里需要设置为0

String key = hashKeyFromUrl(url);
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor != null) {
    OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
    if (downloadUrlToStream(url, outputStream)) {
        editor.commit();
    } else {
        editor.abort();
    }
    mDiskLruCache.flush();
}

//将网络下载的图片写入到缓存文件中
private boolean downloadUrlToStream(String urlString, OutputStream outputStream) {}

private String hashKeyFromUrl(String url) {
    String cacheKey;
    try {
        final MessageDigest mDigist = MessageDigest.getInstance("MD5");
        mDigest.update(url.getBytes());
        cacheKey = bytesToHexString(mDigest.digest());
    } catch(Exception e) {
        cacheKey = String.valueOf(url.hashCode());
    }
    return cacheKey;
}

private String bytesToHexString(byte[] bytes) {
    StringBuilder sb = new StringBuilder();
    for (int i = 0; i < bytes.length; i++) {
        String hex = Integer.toHexString(OxFF & bytes[i]);
        if (hex.length == 1) {
            sb.append('0');
        }
        sb.append(hex);
    }
    return sb.toString();
}
MMKV

SharedPreference缺点

  1. 跨进程不安全
  2. 加载缓慢:异步加载,但是异步加载线程没有设置优先级,如果这时候主线程读取数据需要等待加载线程执行完毕(也就是主线程等待低优先级线程锁的问题)
  3. 全量写入:无论是commit还是apply,即使改动一个条目,也会把全部内容写到文件
  4. 卡顿:异步落盘机制在应用崩溃时会导致数据丢失

SP优化

可以在Application中重写getSharedPreference方法,返回自己实现的sp。我们可以自己将多次读写进行合并

MMKV 通过 mmap 内存映射文件,提供一段可供随时写入的内存块,App 只管往里面写数据,由操作系统负责将内存回写到文件,不必担心 crash 导致数据丢失。

数据组织

数据序列化方面我们选用 protobuf 协议,pb 在性能和空间占用上都有不错的表现。考虑到我们要提供的是通用 kv 组件,key 可以限定是 string 字符串类型,value 则多种多样(int/bool/double 等)。要做到通用的话,考虑将 value 通过 protobuf 协议序列化成统一的内存块(buffer),然后就可以将这些 KV 对象序列化到内存中。

写入优化

标准 protobuf 不提供增量更新的能力,每次写入都必须全量写入。考虑到主要使用场景是频繁地进行写入更新,我们需要有增量更新的能力:将增量 kv 对象序列化后,直接 append 到内存末尾;这样同一个 key 会有新旧若干份数据,最新的数据在最后;那么只需在程序启动第一次打开 mmkv 时,不断用后读入的 value 替换之前的值,就可以保证数据是最新有效的。

空间增长

使用 append 实现增量更新带来了一个新的问题,就是不断 append 的话,文件大小会增长得不可控。例如同一个 key 不断更新的话,是可能耗尽几百 M 甚至上 G 空间,而事实上整个 kv 文件就这一个 key,不到 1k 空间就存得下。这明显是不可取的。我们需要在性能和空间上做个折中:以内存 pagesize 为单位申请空间,在空间用尽之前都是 append 模式;当 append 到文件末尾时,进行文件重整、key 排重,尝试序列化保存排重结果;排重后空间还是不够用的话,将文件扩大一倍,直到空间足够。

数据有效性

考虑到文件系统、操作系统都有一定的不稳定性,我们另外增加了 crc 校验,对无效数据进行甄别。在 iOS 微信现网环境上,我们观察到有平均约 70万日次的数据校验不通过。

Kotlin协程

Kotlin的协程:

Kotlin协程

协程常见面试题

什么是协程

协程指的是一种特殊的函数,它可以在执行到某个位置时暂停 ,并保存当前的执行状态,然后 让出CPU控制权,使得其他代码可以继续执行。当CPU再次调用这个函数时,它会从上次暂停的位置继 续执行,而不是从头开始执行。从而使得程序在执行长时间任务时更加高效和灵活。

协程中的Flow

Kotlin常见面试知识点

Kotlin面试题

Flutter面试题

2. 组件化架构如何划分

基于MVVM的组件化

组件化改造

组件化介绍

组件化改造

有赞微商城组件化

组件化开发的意义
  1. 各个组件专注自身功能的实现,模块中代码高度聚合,只负责一项任务,符合单一职责原则
  2. 各组件高度解耦,各业务研发职责清晰、互不干扰、提升协作效率
  3. 业务组件可进行拔插,灵活多变,复用性高
  4. 加快编译速度,提高开发效率
组件化结构如何划分

image.png

按照业务划分模块,按照功能划分组件,模块中包含很多组件。

从下往上依次是:

  • 基础层组件(不包含任何业务),例如:网络请求的组件、动态权限申请组件、数据库组件、日志组件、埋点组件、通用UI组件、通用工具类等;

  • 业务公共层组件,主要是登陆、支付、页面路由等接口服务的注册

  • 业务实现层,一般按照不同业务划分不同的业务组件,可能通过不同的技术栈实现。

  • APP宿主层,用来整合初始化依赖的各组件,用来进行工程配置、组件配置、启动服务、接口服务注册、页面路由注册等任务。

组件化方案:
单工程方案

Application主module和集成的各业务library module,library独立调试需要通过开关动态配置。

多工程方案

各业务组件工程以aar的方式集成,独立调试是在各业务组件的工程里边单独调试。

多工程方案相较于单工程方案的优点:

代码权限管控,保证代码的权限隔离,安全和稳定性;可以独立运行调试,编译时间更短,开发效率更高。

  • 单工程方案没法做到代码权限管控,也不能做到开发人员职责划分明确,每个开发人员都可以对任意的组件进行修改,显然还是会造成混乱。
  • 多工程把每个组件都分割成单独的工程,代码权限可以明确管控。集成测试时,通过maven引用来集成即可。并且业务组件和业务基础组件也可以 和 基础组件一样,可以给公司其他项目复用。

3. 组件间如何通信?ARouter实现原理,有什么缺点,如何解决?

组件间通信:

服务暴露组件:

  • 服务暴露方组件:只存放 服务接口、服务接口相关的实体类、路由信息、便于服务调用的util等
  • 服务调用方组件:只依赖 服务提供方的暴露组件,如module_home依赖export_cart,而不依赖module_cart
  • 提供服务的组件:需要依赖 自己的暴露组件,并实现服务接口,如module_cart依赖export_cart 并实现其中的服务接口
  • 接口实现注入,依然由ARouter完成,和页面跳转一样使用路由信息。

ARouter使用场景:

  1. 组件间页面跳转
  2. 组件间通信
  3. 跨组件获取Fragment实例
ARouter的核心原理:

在编译期间会调用注解处理器(APT:Anotation Process Tools)的实现类执行process方法,在识别到目标的注解后(比如:@Route,@Provider),通过JavaPoet在特定的路径文件夹下会生成Route、Provider等辅助类,这些类文件会被打包到源码中。在ARouter.init初始化的阶段通过DexFile加载Dex遍历class,通过com.alibaba.android.arouter.routes全限定路径的包名过滤出辅助类,将类的全路径名添加到Map并缓存到本地SP中。遍历全路径类名Map集合,过滤全路径类名后缀分别加载缓存到路由映射表groupsIndex、interceptorsIndex、providersIndex Map集合中缓存。在调用 ARouter.getInstance().build("").navigation() 的时候,构造PostCad,在LogisticsCenter的completion流程中通过PostCard的path、type 实例出相应的对象(Provider),然后添加providers缓存。判断是否绿色通道isGreenChannel, 决定是否执行拦截器。_navigation()中根据PostCard携带的类型执行不同的逻辑:activity构造 Intent跳转;provider获取PostCard携带的provider实例返回;fragment/broadcast 实例后返回。

autoWires注解参数赋值:在inject(this)的时候会获取AutowiredService(ARouter预埋的服务)调用doInject流程,通过全路径类名实例化ISyringe接口的实现类(为autoWires生成的辅助类),然后调用inject方法为autoWires修饰的变量赋值。

组件初始化:
  • 面向接口编程 + 反射扫描实现类:

该方案是基于接口编程,自定义 Application 去实现一个自定义的接口(interface),这个接口中定一些和 Application 生命周期相对应的抽象方法及其他自定义的抽象方法,每个组件去编写一个实现类,该实现类就类似于一个假的自定义 Application,然后在真正的自定义 Application 中去通过反射去动态查找当前运行时环境中所有该接口的实现类,并且去进行实例化,然后将这些实现类收集到一个集合中,在 Application 的对应声明周期方法中去逐一调用对应方法,以实现各实现类能够和 Application 生命周期相同步,并且持有 Application 的引用及 context 上下文对象,这样我们就可以在组件内模拟 Application 的生命周期并初始化SDK和三方库。使用反射还需要做一些异常的处理。该方案是我见过的最常见的方案,在一些商业项目中也见到过。

  • 面向接口编程 + meta-data + 反射:

该方案的后半部分也是和第一种方法一样,通过接口编程实现 Application 的生命周期同步,其实这一步是避免不了的,在我的方案中,后半部分也是这样实现的。不同的是前半部分,也就是如何找到接口的实现类,该方案使用的是 AndroidManifestmeta-data 标签,通过每个组件内的 AndroidManifest 内去声明一个 meta-data 标签,包含该组件实现类的信息,然后在 Application 中去找到这些配置信息,然后通过反射去创建这些实现类的实例,再将它们收集到一个集合中,剩下的操作基本相同了。该方案和第一种方案一样都需要处理很多的异常。这种方案我在一些开源项目中见到过,个人认为过于繁琐,还要处理很多的异常。

资源命名冲突采用资源前缀: android { resourcePrefix "前缀_" }

如何保证组件之间的初始化顺序:

组件初始化优先级顺序

类似线程优先级一样, 为每个组件定义了一个优先级,通过重写getPriority() 方法可以设置组件的优先级。优先级范围从[1-10],默认优先级都为5,下层组件或需要先初始化的组件,优先级设置高一点。这样我们在加载组件的时候,先对所有组件的优先级进行排序,优先级高的排前面,然后再按顺序进行加载组件,就可解决这个问题了。

4. MVC、MVP、MVVM、MVI架构的实现和区别

MVC
  • Model:提供数据
  • View:显示视图
  • Controller:控制业务逻辑

缺点: 业务逻辑代码和视图代码有耦合

MVP
  • Model:提供数据
  • View:显示视图
  • Presenter:聚合业务逻辑的处理,通过接口暴露结果刷新视图

缺点: 回调接口膨胀,维护难度大。不具备生命周期感知管理能力,容易发生内存泄漏。

MVVM
  • Model:提供数据
  • View:显示视图
  • ViewModel:聚合业务逻辑处理,数据通过可观察订阅的LiveData通知UI的数据更新

缺点:
为保证对外暴露的LiveData是不可变的,需要添加不少模板代码并且容易遗忘; View层与ViewModel层的交互比较分散零乱,不成体系

LifeCycle的使用
lifecycle.addObserver(object : LifecycleEventObserver { 

    override fun onStateChanged(source: LifecycleOwner,
        event: Lifecycle.Event) { 
            Log.d(TAG,event.toString())
    }
})

涉及的重要角色

LifecycleRegistry extends Lifecycle
LifecycleRegistry: 持有存储LifecycleEventObserver的集合
mObserverMap,通过addObserver添加LifecycleEventObserver到
mObserverMap缓存,当生命周期改变的时候会调handleLifeCycleEvent()
处理分发生命周期,拿到缓存的LifecycleEventObserver回调
onStateChanged
public interface LifecycleOwner { 
    @NonNull Lifecycle getLifecycle();
}
LifeCycle原理:

感知生命周期的能力时借助ReportFragment(一个透明的Fragment)分发生命周期回调事件(LifeCycle.Event)。

LifeCycle感知组件生命周期的流程:

拿到lifeCycle调用addObserver添加LifeCycleObserver缓存到LifeCycleRegistry中的observerMap中,在ComponentActivity中通过没有界面的ReportFragment感知activity生命周期的变化调用dispatch方法通过activity实例强转拿到lifeCycle遍历observerMap中缓存的LifeCycleObserver调用onStateChanged方法分发生命周期。

LiveData基本使用

LiveData讲解

//数据层
val mData = MutableLiveData<String>()
//视图层 
mData.observe(this, Observer{
    mData-> nameTextView.text = newName}
)
mData.setValue("a")
mData.postValue("b")
基本原理

观察者模式,对LiveData数据添加一个或者多个观察者,在数据改变的时候,组件生命活跃状态下,实现数据的订阅和分发。

常见问题
  1. LiveData如何实现订阅者模式,如何处理发送事件?

自身通过注册观察者,在数据更新时进行数据变更的通知;

  1. LiveData怎么做到生命周期的感知,如何跟LifecycleOnwer绑定?

通过将ObserverLifecyclerOnwer包装成新的对象LifecycleBoundObserver,接收生命周期改变的状态来实现对生命周期的感知。

  1. LiveData 只在 LifecycleOwneractive状态发送通知,是怎么处理的?

非活跃状态数据无法感知生命周期变化,同时在considerNotify时,会判断包装后的观察者是不是活跃状态来决定数据的分发。

  1. LiveData 会自动在 DESTROY 的状态下取消订阅,是怎么处理的?
public void onStateChanged(LifecycleOwner source, Lifecycle.Event event) {
    if (mOwner.getLifecycle().getCurrentState() == DESTROYED) {
        removeObserver(mObserver);
        return;
    }
    activeStateChanged(shouldBeActive());
}

接收到LifecycleDESTROYED状态,会从订阅的map中移除observer来取消订阅。

  1. 生命周期变化后数据处理流程是怎么样的?

onStateChanged ——> activeStateChanged ——> dispatchingValue ——> considerNotify ——> onChanged

  1. 为什么观察者只能与一个LifecycleOwner绑定,而不是多个?

绑定多个的话,LiveData的生命周期感知能力就乱掉了,会有很多问题;

  1. LiveData的粘性事件?

之前的Observer已经订阅并更新数据,mVersionmLastVersion不再保持同步那么再新增一个Observer,他也会立即受到旧的消息通知。

LiveData的特点
  • 实时数据刷新:当组件处于活跃状态或者从不活跃状态到活跃状态时总是能收到最新的数据;
  • 不会发生内存泄漏:observer会在LifecycleOwner状态变为DESTROYED后自动remove;
  • 不会因 Activity 处于STOP等状态而导致崩溃:如果LifecycleOwner生命周期处于非活跃状态,则它不会接收任何 LiveData事件;
  • 不需要手动解除观察:开发者不需要在onPause或onDestroy方法中解除对LiveData的观察,因为LiveData能感知生命周期状态变化,所以会自动管理所有这些操作;
  • 数据始终保持最新状态:数据更新时,若LifecycleOwner为非活跃状态,那么会在变为活跃时接收最新数据。例如,曾经在后台的 Activity 会在返回前台后,observer立即接收最新的数据等;
LiveData数据倒灌
private void considerNotify(ObserverWrapper observer) {
    if (!observer.mActive) {
        return;
    }
    // Check latest state b4 dispatch. Maybe it changed state but we didn't get the event yet.
    //
    // we still first check observer.active to keep it as the entrance for events. So even if
    // the observer moved to an active state, if we've not received that event, we better not
    // notify for a more predictable notification order.
    if (!observer.shouldBeActive()) {
        observer.activeStateChanged(false);
        return;
    }
    if (observer.mLastVersion >= mVersion) {
        return;
    }
    observer.mLastVersion = mVersion;
    observer.mObserver.onChanged((T) mData);
}

原理: if (observer.mLastVersion >= mVersion) { return; }在生命周期组件重建时,ViewModel会恢复activity销毁之前的数据并与新建的activity关联,LiveDatamVersion变量的值也会恢复,但在新建的activity会重新调用LiveDataobserve()方法添加新的observer,LiveDatamLastVersion的值会重置。activity由非活跃状态变为活跃状态的时候LiveData会发起数据的分发,这时候就会发生数据倒灌。

解决方案:  自定义一个LiveData,使用AtomicBoolean标记事件分发的状态,在数据更新的时候设置AtomicBoolean标记为true,事件分发后设置为false,保证每次数据只做一次分发的消费。

基本原理:

观察者模式,对LiveData数据添加一个或者多个观察者,实现数据的订阅与分发;

主要成员:
  • mVersion:LiveData实例的版本标记,创建LiveData以及setValue时会进行更新+1;
  • mObservers:存储Observer包装类型实例的map;
  • mData:使用LiveData保存的需要观察的数据;
  • mPendingData:保存LiveData临时数据的变量,mPendingData == NOT_SET第一次一定是返回true,之后都是返回false,然后到这个值更新完毕之前的会调用mPendingData=NOT_SET,这也是为什么多次调用 postValue()只有最后一个值才有效的原因;
ViewModel

ViewModel解析

ViewModel 的作用是专门存放与界面相关的数据,分担 Activity/Fragment 的逻辑,同时会维护自己独立的生命周期

特点:
  1. 单一职责,将数据从业务中抽离出来。即只要是界面上看的到的数据,相关变量都应该存放在ViewModel,而不是Activity中。
  2. 生命周期长,存在于所属对象(Activity,Fragment)的全部生命周期,在Activity横竖屏重建时不会被销毁。
应用场景:
  1. 横竖屏切换,Activity重建,数据可依然保存
  2. 同一个Activity下,Fragment之间的数据共享
创建方式:
  1. val viewModel = ViewModelProvider(this).get(TestViewModel::class.java)

  2. val viewModel by viewModels()

    上述两种方式最终都是基于 ViewModelProvider.Factory 来生成 ViewModel 实例。

生命周期:

ViewModel 目前只有一个生命周期方法 onCleared(),是在 ViewModel 实例对象被清除的时候回调。

ViewModel实现原理:
  • Activity(Fragment) 的 ViewModel 都存储在 ViewModelStore 中,每个 Activity(Fragment) 都会拥有一个 ViewModelStore 实例
  • ViewModelProvider 负责向使用者提供访问某个 ViewModel 的接口,其内部会持有当前 Activity(Fragment) 的 ViewModelStore,然后将操作委托给 ViewModelStore 完成
  • ViewModel 能在 Activity(Fragment) 在由于配置重建时恢复数据的实现原理是:Activity(指 support library 中的 ComponentActivity) 会将 ViewModelStore 在 Activity(Fragment) 重建之前交给 ActivityThread 中的 ActivityClientRecord 持有,待 Activity(Fragment) 重建完成之后,再从 ActivityClientRecord 中获取 ViewModelStore
  • 如果应用的进程位于后台时,由于系统内存不足被销毁了。即使利用 ViewModel 的也不能在 Activity(Fragment) 重建时恢复数据。因为存储 ViewModel 的 ViewModelStore 是交给 ActivityThread 中的 ActivityClientRecord 暂存的,进程被回收了,ActivityThread 也就会被回收,ViewModelStore 也就被回收了,ViewModel 自然不复存在了

activity因为配置重建不会被销毁的源码:

getLifecycle().addObserver(new LifecycleEventObserver() {
    @Override
    public void onStateChanged(@NonNull LifecycleOwner source,
            @NonNull Lifecycle.Event event) {
        if (event == Lifecycle.Event.ON_DESTROY) {
            if (!isChangingConfigurations()) {
                getViewModelStore().clear();
            }
        }
    }
});
MVI
  • Model:管理状态
  • View:承载视图
  • Intent:承载用户的意图 状态是DataClass

优点: 通过状态的单向流通,解决了 LiveData对外暴露不可变的模版代码和View与ViewModel的分散交互。

5. APK包瘦身

可以通过通过AndroidStudio的Analyze工具分析APK的组成信息

image.png

文件/目录描述
lib/存放so文件,可能会有armeabi、armeabi-v7a、arm64-v8a、x86、x86_64、mips,大多数情况下只需要支持armabi与x86的架构即可,如果非必需,可以考虑拿掉x86的部分
res/存放编译后的资源文件,例如:drawable、layout等等
assets/应用程序的资源,应用程序可以使用AssetManager来检索该资源
META-INF/APK中所有文件的签名摘要等信息
classes.dexclasses文件是Java Class,被DEX编译后可供Dalvik/ART虚拟机所理解的文件格式
resources.arsc编译后的二进制资源文件
AndroidManifest.xml清单文件
动态库优化:

在Android系统中,每一种CPU架构对应一种ABI,主要有:arm64-v8a(最新),armeabi,armeabi-v7a,x86,x86_64,mips,mips64。现在我们只需要配置armeabi-v7a即可:

android{
    defaultConfig{
        ndk{
            abiFilters "armeabi-v7a"
        }
    }
}
国际化资源优化
android{ 
    defaultConfig{ 
    // 只适配需要的区域 resConfigs 'zh'
    }
}
代码压缩/代码混淆

配置代码压缩和混淆,在主module的build.gradle里配置minifyEnabled 为true,即压缩了代码,也混淆了代码:

buildTypes{
    release{
        // 1、是否进行混淆
        minifyEnabled true
        // 2、开启zipAlign可以让安装包中的资源按4字节对齐,这样可以减少应用在运行时的内存消耗
        zipAlignEnabled true
        // 3、移除无用的resource文件:当ProGuard 把部分无用代码移除的时候,
        // 这些代码所引用的资源也会被标记为无用资源,然后
        // 系统通过资源压缩功能将它们移除。
        // 需要注意的是目前资源压缩器目前不会移除values/文件夹中
        // 定义的资源(例如字符串、尺寸、样式和颜色)
        // 开启后,Android构建工具会通过ResourceUsageAnalyzer来检查
        // 哪些资源是无用的,当检查到无用的资源时会把该资源替换
        // 成预定义的版本。主要是针对.png、.9.png、.xml提供了
        // TINY_PNG、TINY_9PNG、TINY_XML这3个byte数组的预定义版本。
        // 资源压缩工具默认是采用安全压缩模式来运行,可以通过开启严格压缩模式来达到更好的瘦身效果。
        shrinkResources true
        proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
    }
}
图片压缩与更换格式

可以更换图片格式,比如webp,svg可以更小,android studio也提供了对应的支持,但是没有最好的格式,只是适用场景不同。图片压缩网站推荐squoosh.app/。1.12M png图片,选择webp格式后,优化后大小39.6 kB

网络图片优化

很多入口较深的高清大图,或者需要经常更新的图片,也许用户根本不看,就没有必要内置在apk中,看时加载即可,如果需要提前占位置,可以用缩略图代替,至于哪些图网络化,需要根据业务与用户体验来权衡。

无用资源优化

通过Android Studio提供的工具检索出项目未被使用的代码、资源。但删除的时候不能一键清除,因为java类有可能是通过反射来调用的,比如项目里有跟JS交互的相关java类,IDE提示未被使用,但实际有被用。还有,资源也可以动态获取来使用,比如getResources().getIdentifier(“name”,“defType”,getPackageName()),如果某个资源仅存在动态获取资源id的方式,那么这个资源也会被检测为未被使用。所以,AS提供的一键清除所有无用资源要慎重:

5ba3b04b7a60485cbf904703da897f0f.png

image.png 在使用AS的工具检测出无用资源的时候,最好手动一个个排查资源是否有使用,避免误删。

可以使用Lint来剔除无用资源和无用代码:

image.png

image.png

image.png

image.png

三方库处理

选择第三方 SDK 或三方库的时候,可以将包大小作为选择的指标之一, 或只引入部分需要的代码针对库做业务裁剪,而不是将整个包的代码都引入进来。

混合开发,动态化方案

比如一些模块可以使用H5的方式实现,H5的资源通过网络动态下载。

6. 内存优化

内存优化

image.png

Bitmap的内存优化

Bitmap占用内存的大小 = Bitmap的宽度 * Bitmap的高度 * 每个像素点所占用的字节数;

图片Bitmap的优化

图片Bitmap的内存优化方式:

改变Bitmap内存大小

  • 通过inSampleSize采样率压缩(改变Bitmap大小)。
  • 通过martix进行压缩(改变Bitmap大小)。Bitmap.createBitmap或者Bitmap.createScaledBitmap方法。
  • 更改Bitmap.Config格式。 通过Bitmap#compress方法压缩,质量压缩 还有一种很重要的压缩方式,通过Bitmap#compress方法,修改quality的值,来改变Bitmap生成的字节流的大小。这种方法不会改变Bitmap占用的内存大小。

质量压缩不会减少图片的像素,它是在保持像素的前提下改变图片的位深及透明度等,来达到压缩图片的目的。图片的长,宽,像素都不变,那么bitmap所占内存大小是不会变的。这里改变的是bitmap对应的字节数组的大小,适合去传递二进制的图片数据,比如微信分享。

内存优化的常见方式
  • 善用线程池。
  • 善用对象池复用对象。
  • Bitmap图片资源压缩,用完及时回收释放内存。
  • 注册器、数据库游标记得及时回收。
  • 使用高效的数据结构,例如:ArrayMap、ArrayList、SparseArray.

内存优化的好处:

  • 1.防止应用发生OOM。
  • 2.降低应用由于内存过大被LMK机制杀死的概率。
  • 3.避免不合理使用内存导致GC次数增多,从而导致应用发生卡顿。

内存的问题

    1. 内存抖动

频繁发生GC的时候内存波动图形呈 锯齿张GC导致卡顿

这个问题在 Dalvik虚拟机 上会 更加明显,而 ART虚拟机 在 内存管理跟回收策略 上都做了 大量优化内存分配和GC效率相比提升了5~10倍,所以 出现内存抖动的概率会小很多

内存泄漏的治理
    1. 内存泄漏

Android系统虚拟机的垃圾回收是通过虚拟机GC机制来实现的。GC会选择一些还存活的对象作为内存遍历的根节点GC Roots,通过对GC Roots的可达性来判断是否需要回收。内存泄漏就是 在当前应用周期内不再使用的对象被GC Roots引用,导致不能回收,使实际可使用内存变小。简言之,就是 对象被持有导致无法释放或不能按照对象正常的生命周期进行释放。一般来说,可用内存减少、频繁GC,容易导致内存泄漏。(生命周期短的对象被生命周期长的对象持有,导致无法被释放)

内存泄漏的场景的避免的方式

1. handler造成的内存泄漏:  使用静态内部类+WeakReference包装强引用类型,在生命周期销毁的方法里边清空handler消息回调,并且置为null。

单例模式造成的内存泄漏(单例的静态特性导致其生命周期同应用一样长)

  1. 原因:单例持有Activity、Service的Context,持有View

解决方案:如果传入Context,使用ApplicationContext。使用弱引用的方式持有View

内部类或者匿名内部类造成的内存泄漏:

解决方案:

  1. 将内部类编程静态内部类
  2. 如果有强引用Activity中的属性,即将该属性的引用方式改为弱引用
  3. 在业务允许的情况下,当Activity执行onDestroy()方法的时候,结束耗时任务(如匿名的Thread)

Activity Context的不正确使用

静态变量持有Activity的引用,静态变量生命周期大于Activity的生命周期导致Activity无法被回收,而造成内存泄漏。

解决方案:

  1. 使用ApplicationContext代替ActivityContext,因为ApplicationContext生命周期与应用相同,不依赖Activity的生命周期
  2. 对Context的引用不要超过它本身的生命周期,慎重的对Context使用static关键字。Context里如果有线程,一定要在onDestroy()里及时停掉

注册监听器的泄漏:

系统服务可以通过Context.getSystemService获取,他们负责执行某些后台任务,或者为硬件访问提供接口。如果Context想要在服务内部发生事件是被通知,呢就需要把自己注册到服务的监听器中。然而,这会让服务持有Activity的引用,如果在Activity onDestroy()时没有释放掉引用就会造成内存泄漏。

解决方案:1. 使用ApplicationContext代替ActivityContext

  1. 在Activity执行onDestroy()时,调用反注册

Cursor、Streame没有close,View没有recyle

集合中对象没有清理造成的内存泄漏。

WebView造成的泄漏。

解决方案:不用时候销毁

避免内存抖动

原因:大量的对象被创建又在短时间内马上被释放(频繁GC)

避免在循环中创建临时对象

避免在onDrow中创建Paint、bitmap对象

使用优化过的数据结构

HashMap,ArrayMap,SparseArray源码分析及性能对比

编程语言提供的某些类未针对在移动设备上使用进行优化。例如,通用 HashMap实现的内存效率可能非常低,因为每个映射都需要单独的入口对象。

Android框架包括若干优化的数据的容器,其中包括 SparseArray,SparseBooleanArray,和LongSparseArray。例如,这些SparseArray类更高效,因为它们避免了系统需要自动 复制 密钥并且有时需要值(这会为每个条目创建另一个对象或两个对象)。

    1. 内存溢出

像内存申请一块内存空间,内存不足以给到的时候就会发生内存泄漏。 除了因内存泄漏累积到一定程度导致OOM的情况以外,也有一次性申请很多内存,比如说 一次创建大的数组或者是载入大的文件如图片的时候会导致OOM。而且,实际情况下 很多OOM就是因图片处理不当 而产生的。

内存分析工具

Memory Profiler

  • 1)、实时图表展示应用内存使用量
  • 2)、用于识别内存泄漏、抖动等
  • 3)、提供捕获堆转储、强制GC以及根据内存分配的能力

优点

  • 1)、方便直观
  • 2)、线下使用

Memory Analyzer

强大的 Java Heap 分析工具,查找 内存泄漏及内存占用, 生成 整体报告分析内存问题 等等。建议 线下深入使用

LeakCanary

自动化 内存泄漏检测神器。建议仅用于线下集成

它的 缺点 比较明显,具体有如下两点:

  • 1)、虽然使用了 idleHandler与多进程,但是 dumphprof 的 SuspendAll Thread 的特性依然会导致应用卡顿
  • 2)、在三星等手机,系统会缓存最后一个Activity,此时应该采用更严格的检测模式
Android内存管理机制回顾

1、Java 内存分配

Java的 内存分配区域 分为如下 五部分

  • 1)、方法区:主要存放静态常量
  • 2)、虚拟机栈:Java变量引用
  • 3)、本地方法栈:native变量引用
  • 4)、堆:对象
  • 5)、程序计数器:计算当前线程的当前方法执行到多少行

内存都懂常见案例:

下面列举一些导致内存抖动的常见案例,如下所示:

1、字符串使用加号拼接

  • 1)、使用StringBuilder替代
  • 2)、初始化时设置容量,减少StringBuilder的扩容

2、资源复用

  • 1)、使用 全局缓存池,以 重用频繁申请和释放的对象
  • 2)、注意 结束 使用后,需要 手动释放对象池中的对象

3、减少不合理的对象创建

  • 1)、ondraw、getView 中创建的对象尽量进行复用
  • 2)、避免在循环中不断创建局部变量

4、使用合理的数据结构

使用 SparseArray类族、ArrayMap 来替代 HashMap

而对于 内存泄漏的分析 一般可简述为如下 两步

  • 1)、使用 Memory Profiler 初步观察
  • 2)、通过 Memory Analyzer 结合代码确认
搭建系统化的图片优化和监控机制(这个很有用)

7. Gilde图片加载框架

Glide四级缓存

Glide四级缓存的讲解 Glide分为四级缓存,分别为activecache,memorycache,diskdatacache,diskresourcecache。

  • activecache 为活动缓存,缓存正在使用的图片;
  • memorycache 为内存缓存;
  • diskdatacache 为磁盘缓存的原图片缓存;
  • diskresourcecache 为磁盘缓存的处理后图片缓存。

其中前两者为内存中的缓存,后两者为磁盘中的缓存。

activecache为面向用户展示的缓存,该级缓存是通过弱引用维护,然后保存在hashmap中。

取:当APP需要请求图片时,首先从该缓存中根据key查询,如果查询得到,那么将该图片的引用次数acquire加1,并将查询得到的图片展示给用户;

清:当用户滑动页面该图片为不可见状态或者页面调用ondestroy方法销毁时,会调用onrelease()方法从activecache中释放该图片。此时会将图片的引用acquire减1,如果acquire<=0说明当前页面已经不引用该图片,那么该图片会从activecache中删除,并存入memorycache中。

存:当用户请求某图片时,如果四级缓存中都未获取到,那么会进行网络请求,请求成功后,会将图片缓存至activecache中。

memorycache:如果用户请求图片在activecache中没找到,那么会继续往memorycache中查找。如果找到该图片,那么返回该图片,并在memrorycache中删除该图片,但在返回之前会给该图片引用acquire加1记录图片引用次数,并且将该图片放进activecache中。该级内存缓存运用了lrucache的原理。如果缓存满了,用最少使用算法清除最不常用的图片(根据图片被引用次数acquire清除)。

对于磁盘缓存,比较灵活可以根据需求自己设置:调用diskCacheStrategy(DiskCacheStrategy.NONE)其中参数有四种:all,none,data,resource,autonatic。 all:表示即缓存原始图片也缓存转换处理过后的图片; none:不进行磁盘缓存; data:只缓存原始图片; resource:只缓存转换处理过后的图片; automatic:默认策略,如果是远程图片,则只缓存原始图片;如果是本地图片,那么只缓存转换过后的图片。 根据以上总结,总的来说只有两种磁盘缓存方式,1.原始图片缓存,2.处理后的图片缓存。并且这两种可以混合使用。 底部原理是采用DiskLruCache做的。

8. APP应用冷启动,Activity冷启动的过程

APP应用冷启动优化

1.主线程优化:

  • MultiDex优化
  • ContentProvider优化:  ContentProvider的onCreate方法在Application的onCreate方法之前执行,可将在应用启动时候需要同步初始化的任务提到ContentProvider的onCreate方法里边进行初始化。(如果当前应用中有多个自己的ContentProvider,可引入JetPack组件StartUp针对ContentProvider进行合并优化)
  • 任务调度分类:  懒加载的任务可以放到使用的时候再去初始化。例如:短信SDK的初始化
  • Activity优化:  在Activity中不要做同步阻塞任务,将任务可以放到异步执行;必要的同步阻塞任务可以放到Handler的idelCallback中执行。
  • UI优化:
  1. 首页布局通过代码组合View的方式构建布局,添加到DecorView节省XML布局加载解析的时间。
  2. 异步预加载方案:在后台线程提前将这些 view 加载到内存,在 measure 阶段再直接从内存中进行读取。
  • 主线程消息调度:  提高启动场景页面相关消息的优先级。

2.后台任务优化:

减少后台线程任务:

  • 减少后台线程不必要的任务的执行,特别是一些重 CPU、IO 的任务;
  • 对启动阶段线程数进行收敛,防止过多的并发任务抢占主线程资源,同时也可以避免频繁的线程间调度降低并发效率(减少线程数,使用线程池)。
App应用冷启动的过程

Android应用启动流程分析

App应用冷启动流程:

  • 启动进程:点击图标发生在Launcher应用的进程,startActivity()函数最终是由Instrumentation通过Android的Binder跨进程通信机制 发送消息给 system_server 进程; 在 system_server 中,启动进程的操作在ActivityManagerService。AMS发现ProcessRecord不存在时,就会执行Process.start(),最终是通过 socket 通信告知 Zygote 进程 fork 子进程(app进程)。

  • 开启主线程:app进程创建后,首先是反射调用android.app.ActivityThread类的main方法,main()函数里会初始化app的运行时环境:创建 ApplicationThread,Looper,Handler 对象,并开启主线程消息循环Looper.loop()

  • 创建并初始化 Application和Activity: ActivityThread的main()调用 ActivityThread#attach(false) 方法进行 Binder 通信,通知system_server进程执行 ActivityManagerService#attachApplication(mAppThread) 方法,用于初始化Application和Activity。 在system_server进程中,ActivityManagerService#attachApplication(mAppThread)里依次初始化了Application和Activity,分别有2个关键函数: - thread#bindApplication() 方法通知主线程Handler 创建 Application 对象、绑定 Context 、执行 Application#onCreate() 生命周期 - mStackSupervisor#attachApplicationLocked() 方法中调用 ActivityThread#ApplicationThread#scheduleLaunchActivity() 方法,进而通过主线程Handler消息通知创建 Activity 对象,然后再调用 mInstrumentation#callActivityOnCreate() 执行 Activity#onCreate() 生命周期。

    1. 布局&绘制

    源码流程可以参考Android View 的绘制流程分析及其源码调用追踪

至此,应用启动流程完成。

其中1、2、3的源码流程可以参考Android Application 启动流程分析及其源码调用探究,但代码细节不是本篇重点。

Android的Binder机制

AMS与ActivityThread的交互主要是通过进程间通信 (IPC) 。跨进程通信的机制就是将方法调用及其数据分解至操作系统可识别的程度,并将其从本地进程和地址空间传输至远程进程和地址空间,然后在远程进程中重新组装并执行该调用。 Android系统提供了执行这些 IPC 事务的方案——Binder机制,因此学习app启动流程需要去了解相关的binder接口和实现。

App进程与SystemServer进程是通过Binder机制进行进程间通信,Android为此设计了两个Binder接口:

  1. IActivityManager: 作为应用进程请求系统进程的接口
  2. IApplicationThread: 作为系统进程请求应用进程的接口

在实现IPC的时候,会先写一个aidl文件,比如IActivityManager.aidl。编译后会生成一个IActivityManager.java文件。里面包含3个东西:

  1. IActivityManager接口,继承IInterface,代表服务端进程对象具备什么样的能力。

  2. Stub,IActivityManager的静态内部抽象类。

    • 实现IActivityManager接口,代表实现了服务端的各种行为
    • 继承Binder,代表实现服务端行为的本地对象
    • Binder又继承IBinder接口,代表的是跨进程通信的能力,只要实现了IBinder接口都可以跨进程传输
  3. Proxy,Stub的静态内部类。

    • 实现IActivityManager接口,作为服务端Binder对象在客户端进程的代理,供客户端调用。

所以Binder接口在客户端和服务端各有一个实现:

  1. Binder接口的两种实现一般从命名上可以区分:xxxNative和xxxProxy
  2. xxxNative继承Stub,并继承Binder,运行服务端进程内,代表服务端进程的本地对象;
  3. xxxProxy运行在客户端进程,是服务端Binder对象在客户端进程的代理;
  4. 它们之间的通信是由客户端端用IBinder的transact()对数据进行序列化并发送请求,服务端用Binder的onTransact()响应执行并返回结果给客户端,这个过程是同步的。
这些Binder都由ServiceManager统一管理:

ServiceManager管理所有的Android系统服务,有人把ServiceManager比喻成Binder机制中的DNS服务器,client端应用如果要使用系统服务,调用getSystemService接口,ServiceManager就会通过字符串形式的Binder名称找到并返回对应的服务的Binder对象。
服务端(系统进程) IActivityManager——ActivityManagerNative——ActivityManagerService

AMS服务统一调度所有app进程的Activity启动:

  1. AMS(ActivityManagerService),继承Binder类,作为服务端的“桩(Stub)”,运行在系统进程,在SystemServer进程启动后完成初始化。在应用启动流程中,充当着服务端的角色。接收app进程的调用,App中Activity的生命周期由AMS管理,它决定着什么时候该调用onCreate、onResume这些生命周期函数,把Activity放在哪个栈里,上下文之间的关系是怎样的等等。
  2. ActivityManagerProxy作为系统进程在app端的“代理(Proxy)”,运行在app进程,其主要职责就是将指令和数据进行“序列化(parcel)”,再传递给系统进程的“桩(Stub)”。
  3. 比如App进程的startActivity,就是通过AMS在客户端的代理ActivityManagerProxy发起的。

比如:

  1. startActivity 最终调用了AMS的 startActivity 系列方法,实现了Activity的启动;Activity的生命周期回调,也在AMS中完成;
  2. startService,bindService 最终调用到AMS的startService和bindService方法;
  3. 动态广播的注册和接收在 AMS 中完成(静态广播在 PMS 中完成)
  4. getContentResolver 最终从 AMS 的 getContentProvider 获取到ContentProvider
客户端 IApplicationThread——ApplicationThreadNative——ActivityThread

每个Activity的启动过程则由其所属的app进程具体来完成:

  1. 桩(Stub):ApplicationThread,它是ActivityThread的一个内部类,ApplicationThread负责响应系统进程发起的请求,运行在app进程。并且通过主线程handler使Activity的创建以及各个生命周期都会运行在主线程。实际的业务逻辑是在ActivityThread中。
  2. 代理(Proxy):ApplicationThreadProxy,服务端的AMS通过客户端的代理ApplicationThreadProxy来通知应用进程执行Activity启动逻辑。还将Activity的状态变化传递到客户端的Activity对象。

和服务端的AMS相对应,ActivityThread在应用启动的Client/Server模式中,是作为客户端那一边的具体实现。虽然类名是ActivityThread,但并不是一个线程,但它包含了应用进程的主线程运作的全部机制:

  • 启动应用的主线程,并开启消息循环
  • 四大组件的创建和生命周期的具体实现。

9. App网络优化

关于Android网络优化你需要了解的知识点

APP网络优化,OkHttp的原理,连接池原理,线程池的调度原理,线程的创建过程, Socket三次握手简历连接,Socket四次挥手断开连接,发送的ack数据包以及参数的 左右和每次握手(或者挥手)当前所处的状态,Retrofit的原理。网络协议模型。 Http和Https的区别。TCP和UDP的区别。

OkHttp详解
基本使用
OkHttpClient okHttpClient = new OkHttpClient.Builder() .build(); 
Request request = new Request.Builder() .url(url) .build(); 
okHttpClient.newCall(request).enqueue(new Callback() { 
@Override public void onFailure(Call call, IOException e) {

    } 
@Override public void onResponse(Call call, Response response) throws IOException {

    } 
});
请求与响应流程
1. 请求的封装
  final class RealCall implements Call {
    
  private RealCall(OkHttpClient client, Request originalRequest, boolean forWebSocket) {
    //我们构建的OkHttpClient,用来传递参数
    this.client = client;
    this.originalRequest = originalRequest;
    //是不是WebSocket请求,WebSocket是用来建立长连接的,后面我们会说。
    this.forWebSocket = forWebSocket;
    //构建RetryAndFollowUpInterceptor拦截器
    this.retryAndFollowUpInterceptor = new RetryAndFollowUpInterceptor(client, forWebSocket);
  }
}

request请求以及参数将会被封装成realCal

2. 请求的发送
  • 异步请求:构造一个AsyncCall,并将自己加入处理队列中
final class RealCall implements Call {
    
      @Override public void enqueue(Callback responseCallback) {
        synchronized (this) {
          if (executed) throw new IllegalStateException("Already Executed");
          executed = true;
        }
        captureCallStackTrace();
        client.dispatcher().enqueue(new AsyncCall(responseCallback));
      }
}    
  • 同步请求:直接执行,并返回请求结果
final class RealCall implements Call {
    @Override public Response execute() throws IOException {
      synchronized (this) {
        if (executed) throw new IllegalStateException("Already Executed");
        executed = true;
      }
      captureCallStackTrace();
      try {
        client.dispatcher().executed(this);
        Response result = getResponseWithInterceptorChain();
        if (result == null) throw new IOException("Canceled");
        return result;
      } finally {
        client.dispatcher().finished(this);
      }
    }
}
3. 请求的调度
   public final class Dispatcher {
      private int maxRequests = 64;
      private int maxRequestsPerHost = 5;
      /** Ready async calls in the order they'll be run. */
      private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
    
      /** Running asynchronous calls. Includes canceled calls that haven't finished yet. */
      private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
    
      /** Running synchronous calls. Includes canceled calls that haven't finished yet. */
      private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();
      
      /** Used by {@code Call#execute} to signal it is in-flight. */
      synchronized void executed(RealCall call) {
        runningSyncCalls.add(call);
      }

      synchronized void enqueue(AsyncCall call) {
      //正在运行的异步请求不得超过64,同一个host下的异步请求不得超过5个
      if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) {
        runningAsyncCalls.add(call);
        executorService().execute(call);
      } else {
        readyAsyncCalls.add(call);
      }
    }
} 
  • readyAsyncCalls:准备运行的异步请求
  • runningAsyncCalls:正在运行的异步请求
  • runningSyncCalls:正在运行的同步请求

Dispatcher:任务调度器

同步请求:  将请求realCall添加到正在运行的同步请求队列runningSyncCalls

异步请求:  在请求realCall添加前判断当前正在执行的异步请求总数量是否超过64,并且 同一个host下的异步请求是否超过5个。如果超过将请求任务添加到等待运行的异步请求队列readyAsyncCalls

4. 请求的处理
    final class RealCall implements Call {
      Response getResponseWithInterceptorChain() throws IOException {
        // Build a full stack of interceptors.
        List<Interceptor> interceptors = new ArrayList<>();
        //这里可以看出,我们自定义的Interceptor会被优先执行
        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 (!forWebSocket) {
          interceptors.addAll(client.networkInterceptors());
        }
        interceptors.add(new CallServerInterceptor(forWebSocket));
    
        Interceptor.Chain chain = new RealInterceptorChain(
            interceptors, null, null, null, 0, originalRequest);
        return chain.proceed(originalRequest);
      }
    }

调用getResponseWithInterceptorChain()将请求任务传递到拦截器链上发起请求任务的处理过程。

拦截器
1. RetryAndFollowUpInterceptor:负责重定向
2. BridgeInterceptor:负责把用户构造的请求转换为发送给服务器的请求,把服务器返回的响应转换为对用户友好的响应
3. CacheInterceptor:负责读取缓存以及更新缓存
4. ConnectInterceptor:负责与服务器建立连接
5. CallServerInterceptor:负责从服务器读取响应的数据
连接机制
1. 建立连接
   connectInterceptor用来完成连接。而真正的连接在RealConnect中实现,连接由连接池`ConnectPool`来管理,
   连接池最多保持5个地址的keep-alive连接,每个keep-alive时长为5分钟,并有异步线程清理无效的连接。
主要由以下两个方法完成连接:
HttpCodec httpCodec = streamAllocation.newStream(client, doExtensiveHealthChecks);
RealConnection connection = streamAllocation.connection();
最终调用`findConnect()`方法完成连接的创建
一. 建立连接的主要流程
1. 查找是否有完整的连接可用:
      完整可用连接的条件:
        1.1 Socket没有关闭
        1.2 输入流没有关闭
        1.3 输出流没有关闭
        1.4 Http2连接没有关闭
2. 连接池中是否有可用的连接,如果有则复用
3. 如果没有可用连接,则创建一个新的连接
4. 开始TCP连接以及TLS握手操作
5. 将新创建的连接加入连接池

上述方法完成后会创建一个RealConnection对象,然后调用该方法的connect()方法建立连接。

2. 连接池
我们知道在负责的网络环境下,频繁的进行建立Sokcet连接(TCP三次握手)和断开Socket(TCP四次分手)是非常消耗网络资源
和浪费时间的,HTTP中的keepalive连接对于降低延迟和提升速度有非常重要的作用。Okhttp支持5个并发KeepAlive,默认
链路生命为5分钟(链路空闲后,保持存活的时间),连接池有ConectionPool实现,对连接进行回收和管理。
public final class ConnectionPool {
    
        private static final Executor executor = new ThreadPoolExecutor(0 /* corePoolSize */,
          Integer.MAX_VALUE /* maximumPoolSize */, 60L /* keepAliveTime */, TimeUnit.SECONDS,
          new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp ConnectionPool", true));
      
        //清理连接,在线程池executor里调用。
        private final Runnable cleanupRunnable = new Runnable() {
          @Override public void run() {
            while (true) {
              //执行清理,并返回下次需要清理的时间。
              long waitNanos = cleanup(System.nanoTime());
              if (waitNanos == -1) return;
              if (waitNanos > 0) {
                long waitMillis = waitNanos / 1000000L;
                waitNanos -= (waitMillis * 1000000L);
                synchronized (ConnectionPool.this) {
                  try {
                    //在timeout时间内释放锁
                    ConnectionPool.this.wait(waitMillis, (int) waitNanos);
                  } catch (InterruptedException ignored) {
                  }
                }
              }
            }
          }
        };
}    

ConectionPool在内部维护了一个线程池,来清理链接,清理任务由cleanup()方法完成,它是一个阻塞操作,首先执行清理,并返回下次需要清理的间隔时间,调用wait()方法释放锁。等时间到了以后,再次进行清理,并返回下一次需要清理的时间,循环往复.

cleanup()清楚连接的流程:
1. 查询此连接内部的StreanAllocation的引用数量。
2. 标记空闲连接。
3. 如果空闲连接超过5个或者keepalive时间大于5分钟,则将该连接清理掉。
4. 返回此连接的到期时间,供下次进行清理。
5. 全部都是活跃连接,5分钟时候再进行清理。
6. 没有任何连接,跳出循环。
7. 关闭连接,返回时间0,立即再次进行清理。

判断闲置连接的方式: 在连接RealConnection里有个StreamAllocation虚引用列表,每创建一个StreamAllocation,就会把它添加进该列表中,如果流关闭以后就将StreamAllocation对象从该列表中移除,正是利用这种引用计数的方式判定一个连接是否为空闲连接。

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

2. 缓存管理
OkHttp缓存机制是基于DiskLruCache做的。Cache类封装了缓存的实现,实现了InternalCache接口。
public interface InternalCache {
  //获取缓存
  Response get(Request request) throws IOException;
  //存入缓存
  CacheRequest put(Response response) throws IOException;
  //移除缓存
  void remove(Request request) throws IOException;
  //更新缓存
  void update(Response cached, Response network);
  //跟踪一个满足缓存条件的GET请求
  void trackConditionalCacheHit();
  //跟踪满足缓存策略CacheStrategy的响应
  void trackResponse(CacheStrategy cacheStrategy);
}    
Socket连接

TCP连接与断开详解

连接的建立(三次握手)

使用 connect() 建立连接时,客户端和服务器端会相互发送三个数据包,请看下图:
转存失败,建议直接上传图片文件

客户端调用 socket() 函数创建套接字后,因为没有建立连接,所以套接字处于CLOSED状态;服务器端调用 listen() 函数后,套接字进入LISTEN状态,开始监听客户端请求。

这个时候,客户端开始发起请求:

  1. 当客户端调用 connect() 函数后,TCP协议会组建一个数据包,并设置 SYN 标志位,表示该数据包是用来建立同步连接的。同时生成一个随机数字 1000,填充“序号(Seq)”字段,表示该数据包的序号。完成这些工作,开始向服务器端发送数据包,客户端就进入了SYN-SEND状态。 df
  2. 服务器端收到数据包,检测到已经设置了 SYN 标志位,就知道这是客户端发来的建立连接的“请求包”。服务器端也会组建一个数据包,并设置 SYN 和 ACK 标志位,SYN 表示该数据包用来建立连接,ACK 用来确认收到了刚才客户端发送的数据包。

服务器生成一个随机数 2000,填充“序号(Seq)”字段。2000 和客户端数据包没有关系。

服务器将客户端数据包序号(1000)加1,得到1001,并用这个数字填充“确认号(Ack)”字段。

服务器将数据包发出,进入SYN-RECV状态。

  1. 客户端收到数据包,检测到已经设置了 SYN 和 ACK 标志位,就知道这是服务器发来的“确认包”。客户端会检测“确认号(Ack)”字段,看它的值是否为 1000+1,如果是就说明连接建立成功。

接下来,客户端会继续组建数据包,并设置 ACK 标志位,表示客户端正确接收了服务器发来的“确认包”。同时,将刚才服务器发来的数据包序号(2000)加1,得到 2001,并用这个数字来填充“确认号(Ack)”字段。

客户端将数据包发出,进入ESTABLISED状态,表示连接已经成功建立。

  1. 服务器端收到数据包,检测到已经设置了 ACK 标志位,就知道这是客户端发来的“确认包”。服务器会检测“确认号(Ack)”字段,看它的值是否为 2000+1,如果是就说明连接建立成功,服务器进入ESTABLISED状态。

至此,客户端和服务器都进入了ESTABLISED状态,连接建立成功,接下来就可以收发数据了。

四次挥手

Client发送一个FIN,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态。

Server收到FIN后,发送一个ACK给Client,确认序号为u + 1(与SYN相同,一个FIN占用一个序号),Server进入CLOSE_WAIT状态。

Server发送一个FIN,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态。

Client收到FIN后,Client进入TIME_WAIT状态(主动关闭方才会进入该状态),接着发送一个ACK给Server,确认序号为w + 1,Server进入CLOSED状态,完成四次挥手。

  1. 同时关闭连接
Http常见的错误码

Http常见的错误码

  • 状态代码 200 = 这是成功 HTTP 请求的标准”确定”状态代码。 返回的响应取决于请求。 例如,对于 GET 请求,响应将包含在消息正文中。 对于 PUT/POST 请求,响应将包括包含操作结果的资源。
  • 状态代码 201 – 这是确认请求成功并由此创建新资源的状态代码。 通常,这是在 POST/PUT 请求后发送的状态代码。
  • 状态代码 204 = 此状态代码确认服务器已满足请求,但不需要返回信息。 此状态代码的示例包括删除请求或请求是通过表单发送的,响应不应导致刷新表单或加载新页面。
  • 状态代码 304 = 用于浏览器缓存的状态代码。 如果响应尚未修改,则客户端/用户可以继续使用相同的响应/缓存版本。 例如,如果资源已自特定时间以来被修改,浏览器可以请求。 如果没有,则发送状态代码 304。 如果已修改,则发送状态代码 200 以及资源。
  • 状态代码 400 = 服务器由于客户端错误而无法理解和处理请求。 缺少数据、域验证和无效格式是导致发送状态代码 400 的一些示例。
  • 状态代码 401 = 当需要身份验证但失败或未提供身份验证时,将发生此状态代码请求。
  • 状态代码 403 – 与状态代码 401 非常相似,状态代码 403 在发送有效请求时发生,但服务器拒绝接受。 如果客户端/用户需要必要的权限,或者他们可能需要帐户来访问资源,则会发生这种情况。 与状态代码 401 不同,身份验证将不适用于此处。
  • 状态代码 404 = 普通用户将看到的最常见状态代码。 当请求有效,但无法在服务器上找到资源时,将发生状态代码 404。 即使这些代码分组在客户端错误”存储桶”中,它们通常是由于不正确的 URL 重定向造成的。
  • 状态代码 409 – 当请求与资源的当前状态冲突时,将发送状态代码 409。 这通常是同时更新或版本相互冲突的问题。
  • 状态代码 410 = 请求的资源不再可用,并且将不再可用。 了解网络错误 410
  • 状态代码 500 – 用户通常看到的另一个状态代码,500 系列代码类似于 400 系列代码,因为它们是真正的错误代码。 当服务器由于意外问题无法完成请求时,将发生状态代码 500。 Web 开发人员通常必须对服务器日志进行梳理,以确定问题的确切问题来自何处。
域名解析失败

DNS解析失败的原因

  1. DNS配置错误:一个最常见的DNS解析失败的原因是DNS服务器配置错误。DNS配置错误可能包括域名注册、DNS记录、IP地址和CNAME记录等。

  2. DNS缓存过期:DNS缓存是DNS解析过程中的一个非常重要的部分。如果DNS中缓存了错误的记录或过期的记录,则会引起DNS解析失败。

  3. ISP问题:DSL、电缆和其他类型的互联网服务提供商(ISP)可能也会出现DNS解析失败的问题。这通常是由于ISP的DNS服务器无法解析特定的域名或IP地址造成的。

Http对头阻塞

Http对头阻塞

网络协议——七层、五层、四层协议概念及功能分析

网络协议——七层、五层、四层协议概念及功能分析

http 与 https 的区别?https 是如何工作的?

http 是超文本传输协议,而 https 可以简单理解为安全的 http 协议。https 通过在 http 协议下添加了一层 ssl 协议对数据进行加密从而保证了安全。https 的作用主要有两点:建立安全的信息传输通道,保证数据传输安全;确认网站的真实性。

http 与 https 的区别主要如下:
  • https 需要到 CA 申请证书,很少免费,因而需要一定的费用
  • http 是明文传输,安全性低;而 https 在 http 的基础上通过 ssl 加密,安全性高
  • 二者的默认端口不一样,http 使用的默认端口是80;https使用的默认端口是 443
https 的工作流程

提到 https 的话首先要说到加密算法,加密算法分为两类:对称加密和非对称加密。

  • 对称加密: 加密和解密用的都是相同的秘钥,优点是速度快,缺点是安全性低。常见的对称加密算法有 DES、AES 等等。
  • 非对称加密: 非对称加密有一个秘钥对,分为公钥和私钥。一般来说,私钥自己持有,公钥可以公开给对方,优点是安全性比对称加密高,缺点是数据传输效率比对称加密低。采用公钥加密的信息只有对应的私钥可以解密。常见的非对称加密包括RSA等。

在正式的使用场景中一般都是对称加密和非对称加密结合使用,使用非对称加密完成秘钥的传递,然后使用对称秘钥进行数据加密和解密。二者结合既保证了安全性,又提高了数据传输效率。

https 的具体流程如下:
  1. 客户端(通常是浏览器)先向服务器发出加密通信的请求
  • 支持的协议版本,比如 TLS 1.0版
  • 一个客户端生成的随机数 random1,稍后用于生成"对话密钥"
  • 支持的加密方法,比如 RSA 公钥加密
  • 支持的压缩方法
  1. 服务器收到请求,然后响应
  • 确认使用的加密通信协议版本,比如 TLS 1.0版本。如果浏览器与服务器支持的版本不一致,服务器关闭加密通信
  • 一个服务器生成的随机数 random2,稍后用于生成"对话密钥"
  • 确认使用的加密方法,比如 RSA 公钥加密
  • 服务器证书
  1. 客户端收到证书之后会首先会进行验证
  • 首先验证证书的安全性
  • 验证通过之后,客户端会生成一个随机数 pre-master secret,然后使用证书中的公钥进行加密,然后传递给服务器端
  1. 服务器收到使用公钥加密的内容,在服务器端使用私钥解密之后获得随机数 pre-master secret,然后根据 radom1、radom2、pre-master secret 通过一定的算法得出一个对称加密的秘钥,作为后面交互过程中使用对称秘钥。同时客户端也会使用 radom1、radom2、pre-master secret,和同样的算法生成对称秘钥。
  2. 然后再后续的交互中就使用上一步生成的对称秘钥对传输的内容进行加密和解密。
http头部的字段以及含义
  • Accept : 浏览器(或者其他基于HTTP的客户端程序)可以接收的内容类型(Content-types),例如 Accept: text/plain
  • Accept-Charset: 浏览器能识别的字符集,例如 Accept-Charset: utf-8
  • Accept-Encoding: 浏览器可以处理的编码方式,注意这里的编码方式有别于字符集,这里的编码方式通常指gzip,deflate等。例如 Accept-Encoding: gzip, deflate
  • Accept-Language: 浏览器接收的语言,其实也就是用户在什么语言地区,例如简体中文的就是 Accept-Language: zh-CN
  • Authorization: 在HTTP中,服务器可以对一些资源进行认证保护,如果你要访问这些资源,就要提供用户名和密码,这个用户名和密码就是在Authorization头中附带的,格式是“username:password”字符串的base64编码
  • Cache-Control: 这个指令在request和response中都有,用来指示缓存系统(服务器上的,或者浏览器上的)应该怎样处理缓存,因为这个头域比较重要,特别是希望使用缓 存改善性能的时候
  • Connection: 告诉服务器这个user agent(通常就是浏览器)想要使用怎样的连接方式。值有keep-alive和close。http1.1默认是keep-alive。keep-alive就是浏览器和服务器 的通信连接会被持续保存,不会马上关闭,而close就会在response后马上关闭。但这里要注意一点,我们说HTTP是无状态的,跟这个是否keep-alive没有关系,不要认为keep-alive是对HTTP无状态的特性的改进。
  • Cookie: 浏览器向服务器发送请求时发送cookie,或者服务器向浏览器附加cookie,就是将cookie附近在这里的。例如:Cookie:user=admin
  • Content-Length: 一个请求的请求体的内存长度,单位为字节(byte)。请求体是指在HTTP头结束后,两个CR-LF字符组之后的内容,常见的有POST提交的表单数据,这个Content-Length并不包含请求行和HTTP头的数据长度。
  • Content-MD5: 使用base64进行了编码的请求体的MD5校验和。例如:Content-MD5: Q2hlY2sgSW50ZWdyaXR5IQ==
  • Content-Type: 请求体中的内容的mime类型。通常只会用在POST和PUT方法的请求中。例如:Content-Type: application/x-www-form-urlencoded
  • Date: 发送请求时的GMT时间。例如:Date: Tue, 15 Nov 1994 08:12:31 GMT
  • From: 发送这个请求的用户的email地址。例如:From: user@example.com
  • Host: 被服务器的域名或IP地址,如果不是通用端口,还包含该端口号,例如:Host: www.some.com:182
  • Proxy-Authorization: 连接到某个代理时使用的身份认证信息,跟Authorization头差不多。例如:Proxy-Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==
  • User-Agent: 通常就是用户的浏览器相关信息。例如:User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:12.0) Gecko/20100101 Firefox/12.0
  • Warning: 记录一些警告信息。

10. EventBus的原理,AOP的原理,字节码插桩的原理

EventBus3.0

EventBus3.0原理

EventBus工作原理总结

这里将EventBus的工作原理分为三步来说明:

  1. EventBus的注册与订阅
  2. EventBus的事件发布
  3. EventBus的注销
EventBus的事件注册
EventBus.getDefault().register(this)

image.png 传入注册类信息,根据反射获取注册类上的所有方法,遍历这些方法,取出其中的订阅方法(条件是,一个参数event,权限为public,使用了Subscribe标签)将方法的信息封装成SubscriberMethod对象,并存入集合,然后再遍历这个集合,取出其中的SubscriberMethod对象,再根据注册类的字节码文件,合并成Subscription对象,再根据event类型,进行重新分类,存入名叫subscriptionsByEventType的map中(key 为event, value 为SubscriberMethod对象集合List),再创建名为typesBySubscriber的map,注册类为key , list为value。 完事了。

EventBus取消事件注册
EventBus.getDefault().unregister(this);

image.png 解除绑定,其实比较简单,主要就是运用注册时所产生的2个map, 先根据typesBySubscriber,也就是根据要解除绑定的注册类,找到这个类所拥有的所有订阅事件,然后遍历这些订阅事件,再根据这些订阅事件,在subscriptionsByEventType中找到,这个事件所对应的订阅方法的集合,再遍历集合,判断该订阅方法的注册类信息,是否是要解除绑定的注册类,如果是,移除该订阅方法信息,完成解除绑定。

EventBus的事件发布
EventBus.getDefault().post(new Object());

image.png post也不难,首先是将发送的事件保存在postingState中的队列里面,它是线程独有的,然后遍历postingState中的事件队列,拿出该线程下,所有的事件的集合,然后遍历它,再根据subscriptionsByEventType,取出该事件所对应的所有订阅方法,然后看是否能够直接处理,如果能,直接反射调用订阅方法,如果不能,直接通过HandlerPower、BackgroundPower、AsyncPower切换线程后,再进行反射调用处理。

其中HandlerPower内部就直是封装了个Handler,每次调用的时候,先将事件加入到队列中,然后根据Handler切换到主线程,按顺序取出队列中的事件,反射执行。

BackgroundPower是封装了catchThreadPool用于执行任务, AsyncPower与它类似,但是里面没有同步锁,每次执行都会新开辟一个子线程去执行任务。而BackgroundPower只会开一个线程。

AOP的原理
字节码插桩

Android字节码插桩流程全解析

Bytex插件开发实战

字节码插件平台 ByteX解析

11. H5秒开优化,原生和JS之间的相互通信,富文本编辑器的开发

image.png

image.png

WebView 优化(2)—— 桥接设计、独立进程、跨进程通信

Android WebView H5 秒开方案总结

采用Hybird开发的优缺点
  • 优点:实现跨平台和动态更新、保持各端之间业务和逻辑的统一、满足快速开发的需求;
  • 缺点:性能相对 Native 来说要差得多。
1. 离线包
1. 精简并抽取公共的 JS 和 CSS 文件作为通用的页面模板,打包时放置在客
户端中。内置版本号,在app后台静默更新。
2. 合并多次IO,WebView 会在加载完主 HTML 之后才去加载 HTML 中的 JS 
和 CSS 文件,先后需要进行多次 IO 操作,我们可以将 JS 和 CSS 还有一些
图片都内联到一个文件中,这样加载模板时就只需要一次 IO 操作,也大大减少
了因为 IO 加载冲突导致模板加载失败的问题。
2. 正文数据预请求
在页面跳转拦截中做正文数据的预请求。
3. 模版预加载
本地有缓存H5页面模版的情况下,可以让webView提前加载模版预热。
4. 延迟执行非首屏渲染操作
对于一些非首屏必需的网络请求、 JS 调用、埋点上报等,都可以后置到首屏显
示后再执行。
5. 静态页面直出
并行请求正文数据虽然能够缩短总耗时,但还是需要完成解析 JSON、构造 
DOM、应用 CSS 样式等一系列耗时操作,最终才能交由内核进行渲染上屏,此时 
**组装 HTML** 这个操作就显得比较耗时了。为了进一步缩短总耗时,可以改
为由后端对正文数据和前端代码进行整合,直出首屏内容,直出后的 HTML 文件
已经包含了首屏展现所需的内容和样式,无需进行二次加工,内核可以直接渲
染。其它动态内容可以在渲染完首屏后再进行异步加载

由于客户端可能向用户提供了控制 WebView 字体大小,夜间模式的选项,为了
保证首屏渲染结果的准确性,服务端直出的 HTML 就需要预留一些占位符用于后
续动态回填,客户端在 loadUrl 之前先利用正则匹配的方式查找这些占位字
符,按照协议映射成端信息。经过客户端回填处理后的 HTML 内容就已经具备了
展现首屏的所有条件
6. H5页面的图片格式采用WebP
7. DNS优化
最好就是保持客户端整体 API 地址、资源文件地址、WebView 线上地址的主域
名都是一致的。
8. CDN加速
CDN 的全称是 Content Delivery Network,即内容分发网络。CDN 是构建
在现有网络基础之上的智能虚拟网络,依靠部署在各地的边缘服务器,通过中心
平台的负载均衡、内容分发、调度等功能模块,使用户就近获取所需内容,降低
网络拥塞,提高用户访问响应速度和命中率

通过将 JS、CSS、图片、视频等静态类型文件托管到 CDN,当用户加载网页
时,就可以从地理位置上最接近它们的服务器接收这些文件,解决了远距离访问
和不同网络带宽线路访问造成的网络延迟情
原生和JS之间的相互通信
Android 调 js 有两种方法
  • WebView.loadUrl("javascript:js中的方法名")。 这种方法的优点是很简洁,缺点是没有返回值,如果需要拿到js方法的返回值则需要js调用Android中的方法来拿到这个返回值。
  • WebView.evaluateJavaScript("javascript:js中的方法名",ValueCallback)。 这种方法比 loadUrl 好的是可以通过 ValueCallback 这个回调拿到 js方法的返回值。缺点是这个方法 Android4.4 才有,兼容性较差。不过放在 2018 年来说,市面上绝大多数 App 都要求最低版本是 4.4 了,所以我认为这个兼容性问题不大。
js 调 Android有三种方法:
  • WebView.addJavascriptInterface()。 这是官方解决 js 调用 Android 方法的方案,需要注意的是要在供 js 调用的 Android 方法上加上 @JavascriptInterface 注解,以避免安全漏洞。这种方案的缺点是 Android4.2 以前会有安全漏洞,不过在 4.2 以后已经修复了。同样,在 2018 年来说,兼容性问题不大。

  • 重写 WebViewClient的shouldOverrideUrlLoading()方法来拦截url, 拿到 url 后进行解析,如果符合双方的规定,即可调用 Android 方法。优点是避免了 Android4.2 以前的安全漏洞,缺点也很明显,无法直接拿到调用 Android 方法的返回值,只能通过 Android 调用 js 方法来获取返回值。

  • 重写 WebChromClient 的 onJsPrompt() 方法,同前一个方式一样,拿到 url 之后先进行解析,如果符合双方规定,即可调用Android方法。最后如果需要返回值,通过 result.confirm("Android方法返回值") 即可将 Android 的返回值返回给 js。方法的优点是没有漏洞,也没有兼容性限制,同时还可以方便的获取 Android 方法的返回值。其实这里需要注意的是在 WebChromeClient 中除 了 onJsPrompt 之外还有 onJsAlert 和 onJsConfirm 方法。那么为什么不选择另两个方法呢?原因在于 onJsAlert 是没有返回值的,而 onJsConfirm 只有 true 和 false 两个返回值,同时在前端开发中 prompt 方法基本不会被调用,所以才会采用 onJsPrompt。

富文本编辑器的开发

富文本编辑器开源库

仿今日头条富文本编辑器

今日头条富文本编辑器

12. APP的卡顿优化,卡顿发生的原因,原因采集,卡顿治理,卡顿监控

Android卡顿优化分析及解决方案

13. IM即时通讯功能的集成开发,表的设计,数据存储,数据同步
14. APP的打包编译过程,Gradle的执行构建、Task的执行顺序,CI/CD打包流水线构建
APP的打包编译过程

Android APK编译打包流程

image.png

Android应用的构建流程
  • 编译器将您的源代码转换成 DEX 文件(Dalvik 可执行文件,其中包括在 Android 设备上运行的字节码),并将其他所有内容转换成编译后的资源。
  • 打包器将 DEX 文件和编译后的资源组合成 APKAAB(具体取决于所选的 build 目标)。
  • 打包器使用调试或发布密钥库为 APKAAB 签名。
  • 在生成最终 APK 之前,打包器会使用 zipalign 工具对应用进行优化,以减少其在设备上运行时所占用的内存
Gradle的执行构建、Task的执行顺序
CI/CD打包流水线构建

Gitlab CI/CD打包流水线构建

15. 视频播放器集成开发,视频秒播优化、视频数据格式,实现类似抖音的上下滑动视频播放效果,RecyclerView的四级缓存,YUV和NV21的数据格式,Glide图片加载框架
16. 线程池的实现,线程的治理,收敛

Android线程优化

17. 社区业务内容生产,图片和视频的处理

SurfaceTexture,TextureView, SurfaceView 和 GLSurfaceView 区别知多少

聊聊SurfaceView和TextureView

TextureView和SurfaceView的优缺点
SurfaceViewTextureView
内存
耗电
绘制效率及时1 ~ 3帧的延迟
截图不支持支持
动画不支持支持

不过GLSurfaceView是SurfaceView的子类,除了拥有SurfaceView的优点,GLSurfaceView也支持截图和动画操作。

18. 常见的设计模式
19. 常用的数据结构
20. Android SDK各版本之间的差异

Android历来新特性概览

Android 12的新特性

Android 12的新特性

  1. Material You;
  2. Widgets 的改进;
  3. 富媒体内容的插入;
  4. 应用启动动画;
  5. 触感反馈;
  6. 应用搜索;
  7. 安全与隐私;
  8. 多媒体。
Android 13的新特性

Android 13的新特性 Android 13的新特性适配指南

Android 14的新特性

Android 14的新特性

  • ScreenShot Detection,截屏感知

  • TextView Highlight,文本高亮

  • New System Back Design,全新的系统返回设计

  • Custom Action on Share Sheet,支持自定义操作的系统分享

  • Locale Preferences,区域偏好

  • Grammar Gender,语法性别

  • Path Iterator,路径迭代器

  • Package Installer improvement,安装改善

21. 常见的面试算法题
SparseArray的原理:

SparseArray,通常来讲是 Android 中用来替代 HashMap 的一个数据结构。 准确来讲,是用来替换key为 Integer 类型,value为Object 类型的HashMap。需要注意的是 SparseArray 仅仅实现了 Cloneable 接口,所以不能用Map来声明。 从内部结构来讲,SparseArray 内部由两个数组组成,一个是 int[]类型的 mKeys,用来存放所有的键;另一个是 Object[]类型的 mValues,用来存放所有的值。 最常见的是拿 SparseArray 跟HashMap 来做对比,由于 SparseArray 内部组成是两个数组,所以占用内存比 HashMap 要小。我们都知道,增删改查等操作都首先需要找到相应的键值对,而 SparseArray 内部是通过二分查找来寻址的,效率很明显要低于 HashMap 的常数级别的时间复杂度。提到二分查找,这里还需要提一下的是二分查找的前提是数组已经是排好序的,没错,SparseArray 中就是按照key进行升序排列的。 综合起来来说,SparseArray 所占空间优于 HashMap,而效率低于 HashMap,是典型的时间换空间,适合较小容量的存储。 从源码角度来说,我认为需要注意的是 SparseArray的remove()、put()gc()方法。

  • remove() SparseArray 的 remove() 方法并不是直接删除之后再压缩数组,而是将要删除的 value 设置为 DELETE 这个 SparseArray 的静态属性,这个 DELETE 其实就是一个 Object 对象,同时会将 SparseArray 中的 mGarbage 这个属性设置为 true,这个属性是便于在合适的时候调用自身的 gc()方法压缩数组来避免浪费空间。这样可以提高效率,如果将来要添加的key等于删除的key,那么会将要添加的 value 覆盖 DELETE。
  • gc()。 SparseArray 中的 gc() 方法跟 JVM 的 GC 其实完全没有任何关系。``gc()` 方法的内部实际上就是一个for循环,将 value 不为 DELETE 的键值对往前移动覆盖value 为DELETE的键值对来实现数组的压缩,同时将 mGarbage 置为 false,避免内存的浪费。
  • put()。 put 方法是这么一个逻辑,如果通过二分查找 在 mKeys 数组中找到了 key,那么直接覆盖 value 即可。如果没有找到,会拿到与数组中与要添加的 key 最接近的 key 索引,如果这个索引对应的 value 为 DELETE,则直接把新的 value 覆盖 DELET 即可,在这里可以避免数组元素的移动,从而提高了效率。如果 value 不为 DELETE,会判断 mGarbage,如果为 true,则会调用 gc()方法压缩数组,之后会找到合适的索引,将索引之后的键值对后移,插入新的键值对,这个过程中可能会触发数组的扩容。

说一个你做的亮点项目

  1. 玩物得志的社区

  2. 得物H5秒开优化的技术改造项目

说一个你在项目中遇到的问题,以及怎么解决的

你关注的技术(或者说你最近在看什么书籍)

反问环节

  1. 该岗位属于什么部门,参与或者负责的业务是什么?组内有几个成员?
  2. 对候选人的要求是什么?期望候选人具备什么样的能力?什么样的角色定位?
  3. 面试的话一共会有几轮?

Android基础

学习博文

底层
  1. ART Hook
音视频
  1. Android 音视频开发打怪升级:FFmpeg音视频编解码篇】二、Android 引入FFmpeg
OpenGL

高价值博文

  1. Android启动原理解析
  2. 卡顿监测 · 方案篇 · Android 卡顿监测指导原则
  3. 并发编程 · 基础篇(下) · 三大分析法分析线程池
  4. 内存优化 · 基础论 · 初识Android内存优化
  5. 并发编程 · 基础篇 · android 线程那些事
  6. APM监控 · 入门篇 · Android端测监控平台建设
  7. Android 字节码插桩库,也许有你需要的
  8. ASM 字节码插桩:进行线程整治
  9. 聊聊Android线程优化这件事

Android经典面试题

  1. Android 面试题:手指从按钮 A 平移到 B,会发生什么?为什么?
  2. Kotlin 中修饰函数的 inline、noinline 和 corssinline 到底干啥用的?
MVP、MVVM、MVI架构模式

MVP:

  • 模型(Model): 负责数据和业务逻辑。
  • 视图(View): 负责显示数据和处理用户输入,但不处理业务逻辑。
  • 主持人(Presenter): 充当视图和模型之间的中介,处理业务逻辑、处理用户输入并更新视图。

在MVP中,视图和模型之间通过主持人进行通信,模型和视图是相互独立的,视图不处理业务逻辑。

MVVM:

  • 模型(Model): 负责数据和业务逻辑。
  • 视图(View): 负责显示数据,处理用户输入,但不处理业务逻辑。
  • 视图模型(ViewModel): 处理业务逻辑,管理视图的状态和行为,并向视图公开可绑定的属性。视图通过数据绑定直接从视图模型获取和显示数据。

在MVVM中,视图和视图模型之间通过数据绑定实现通信,视图模型负责处理业务逻辑,并且视图和模型是相互独立的。

MVI:

  • 模型(Model): 负责数据和业务逻辑。
  • 视图(View): 负责显示数据和用户界面。
  • 意图(Intent): 表示用户的操作或意图,由用户界面生成并发送给模型。模型根据意图的变化来更新数据和状态。

在MVI中,模型是单向的,它响应来自视图的意图并更新状态。视图只是触发用户意图的生成,并展示模型的状态。

Android部分

Android高频面试题

    1. 跨进程通信方式有哪些?AIDL通信原理和实现?Binder通信原理?

Flutter部分

Flutter高价值博文

  1. 在 Flutter 中实现最佳 UX 性能的 12 个图像技巧和最佳实践
  2. Flutter应用框架搭建(四) 网络请求封装

原理博文篇

  1. ValueNotifier及ValueListenableBuilder源码解析
  2. Flutter里的状态机制

Flutter高频面试题

    1. Dart多线程实现。Isolate之间数据共享吗?Isolate之间如何通信?
在Flutter中,你可以使用Isolate来创建多线程,而Isolate之间的通信通常使用SendPort和ReceivePort。


Isolate 是 Dart 中的并发模型,不同的 Isolate 是相互隔离的,它们拥有独立的内存空间。因此,默认情况
下,Isolate 之间是不能直接共享数据的。

然而,你可以使用 Dart 的 Isolate 通信机制来在 Isolate 之间传递数据。这通常是通过发送消息和接收消息来
实现的,使用 SendPort 和 ReceivePort。这种方式允许你在 Isolate 之间传递数据,但实际上是在拷贝数
据,而不是共享内存。

如果你需要在 Isolate 之间共享数据,一种选择是使用 Dart 的 Isolate 所提供的 dart:typed_data 库中
的共享内存机制,例如 ByteData。这样的机制会允许多个 Isolate 访问相同的内存空间,但需要小心处理并发访
问和同步的问题。
  • 2. Dart中的Future async await会阻塞主流程吗?

    1. dart:typed_data 库中的共享内存机制原理

Flutter打包

Flutter混合开发

BloC

Bloc模式和Cubit模式区别:

  • 复杂度: BLoC:BLoC通常用于管理更复杂的业务逻辑和状态。它可以处理多个事件和多个状态,适用于大型、复杂的应用程序。 Cubit:Cubit的设计更简单,适用于处理较简单的状态和事件。它更适合于小型和中型应用程序,以及对状态管理模式不太熟悉的开发者。

  • 事件和状态管理: BLoC:在BLoC中,事件和状态是分开管理的,你需要自己编写事件和状态的类,并在BLoC中进行映射。这使得BLoC更加灵活,但也更加复杂。 Cubit:Cubit将事件和状态合并到一个类中,称为Cubit类。这使得代码更加简洁,但在处理复杂的事件和状态时可能会变得混乱。

  • 异步支持: BLoC:BLoC天生支持异步操作,可以轻松处理异步任务,例如网络请求。这是因为BLoC的mapEventToState方法可以返回Stream。 Cubit:Cubit也支持异步操作,但需要使用Emit函数来发射新的状态。

Flutter与原生通信

Flutter定义了三种不同类型的Channel:

  • BasicMessageChannel:双向,持续通信,接收到信息后可回复此消息,用于传递字符串和半结构信息;
  • EventChannel:单向,native → flutter,用于native向flutter发送实时数据,如电量变化、传感器等;
  • MethodChannel:双向,方法互调传参,常用于访问原生设备信息、拍照、定位等;