乐趣区

史上最全的Android面试题集锦

Android 基本知识点

1、常规知识点

1、Android 类加载器

在 Android 开发中,不管是插件化还是组件化,都是基于 Android 系统的类加载器 ClassLoader 来设计的。只不过 Android 平台上虚拟机运行的是 Dex 字节码,一种对 class 文件优化的产物,传统 Class 文件是一个 Java 源码文件会生成一个.class 文件,而 Android 是把所有 Class 文件进行合并、优化,然后再生成一个最终的 class.dex,目的是把不同 class 文件重复的东西只需保留一份,在早期的 Android 应用开发中,如果不对 Android 应用进行分 dex 处理,那么最后一个应用的 apk 只会有一个 dex 文件。

Android 中常用的类加载器有两种,DexClassLoader 和 PathClassLoader,它们都继承于 BaseDexClassLoader。区别在于调用父类构造器时,DexClassLoader 多传了一个 optimizedDirectory 参数,这个目录必须是内部存储路径,用来缓存系统创建的 Dex 文件。而 PathClassLoader 该参数为 null,只能加载内部存储目录的 Dex 文件。所以我们可以用 DexClassLoader 去加载外部的 apk 文件,这也是很多插件化技术的基础。

2、Service

理解 Android 的 Service,可以从以下几个方面来理解:

  • Service 是在 main Thread 中执行,Service 中不能执行耗时操作(网络请求,拷贝数据库,大文件)。
  • 可以在 xml 中设置 Service 所在的进程,让 Service 在另外的进程中执行。
  • Service 执行的操作最多是 20s,BroadcastReceiver 是 10s,Activity 是 5s。
  • Activity 通过 bindService(Intent,ServiceConnection,flag)与 Service 绑定。
  • Activity 可以通过 startService 和 bindService 启动 Service。

IntentService

IntentService 是一个抽象类,继承自 Service,内部存在一个 ServiceHandler(Handler)和 HandlerThread(Thread)。IntentService 是处理异步请求的一个类,在 IntentService 中有一个工作线程(HandlerThread)来处理耗时操作,启动 IntentService 的方式和普通的一样,不过当执行完任务之后,IntentService 会自动停止。另外可以多次启动 IntentService,每一个耗时操作都会以工作队列的形式在 IntentService 的 onHandleIntent 回调中执行,并且每次执行一个工作线程。IntentService 的本质是:封装了一个 HandlerThread 和 Handler 的异步框架。

2.1、生命周期示意图

Service 作为 Android 四大组件之一,应用非常广泛。和 Activity 一样,Service 也有一系列的生命周期回调函数,具体如下图。

通常,启动 Service 有两种方式,startService 和 bindService 方式。

2.2、startService 生命周期

当我们通过调用了 Context 的 startService 方法后,我们便启动了 Service,通过 startService 方法启动的 Service 会一直无限期地运行下去,只有在外部调用 Context 的 stopService 或 Service 内部调用 Service 的 stopSelf 方法时,该 Service 才会停止运行并销毁。

onCreate

onCreate: 执行 startService 方法时,如果 Service 没有运行的时候会创建该 Service 并执行 Service 的 onCreate 回调方法;如果 Service 已经处于运行中,那么执行 startService 方法不会执行 Service 的 onCreate 方法。也就是说如果多次执行了 Context 的 startService 方法启动 Service,Service 方法的 onCreate 方法只会在第一次创建 Service 的时候调用一次,以后均不会再次调用。我们可以在 onCreate 方法中完成一些 Service 初始化相关的操作。

onStartCommand

onStartCommand: 在执行了 startService 方法之后,有可能会调用 Service 的 onCreate 方法,在这之后一定会执行 Service 的 onStartCommand 回调方法。也就是说,如果多次执行了 Context 的 startService 方法,那么 Service 的 onStartCommand 方法也会相应的多次调用。onStartCommand 方法很重要,我们在该方法中根据传入的 Intent 参数进行实际的操作,比如会在此处创建一个线程用于下载数据或播放音乐等。

public @StartResult int onStartCommand(Intent intent, @StartArgFlags int flags, int startId) {
}

当 Android 面临内存匮乏的时候,可能会销毁掉你当前运行的 Service,然后待内存充足的时候可以重新创建 Service,Service 被 Android 系统强制销毁并再次重建的行为依赖于 Service 中 onStartCommand 方法的返回值。我们常用的返回值有三种值,START_NOT_STICKYSTART_STICKYSTART_REDELIVER_INTENT,这三个值都是 Service 中的静态常量。

START_NOT_STICKY

如果返回 START_NOT_STICKY,表示当 Service 运行的进程被 Android 系统强制杀掉之后,不会重新创建该 Service,当然如果在其被杀掉之后一段时间又调用了 startService,那么该 Service 又将被实例化。那什么情境下返回该值比较恰当呢?如果我们某个 Service 执行的工作被中断几次无关紧要或者对 Android 内存紧张的情况下需要被杀掉且不会立即重新创建这种行为也可接受,那么我们便可将 onStartCommand 的返回值设置为 START_NOT_STICKY。举个例子,某个 Service 需要定时从服务器获取最新数据:通过一个定时器每隔指定的 N 分钟让定时器启动 Service 去获取服务端的最新数据。当执行到 Service 的 onStartCommand 时,在该方法内再规划一个 N 分钟后的定时器用于再次启动该 Service 并开辟一个新的线程去执行网络操作。假设 Service 在从服务器获取最新数据的过程中被 Android 系统强制杀掉,Service 不会再重新创建,这也没关系,因为再过 N 分钟定时器就会再次启动该 Service 并重新获取数据。

START_STICKY

如果返回 START_STICKY,表示 Service 运行的进程被 Android 系统强制杀掉之后,Android 系统会将该 Service 依然设置为 started 状态(即运行状态),但是不再保存 onStartCommand 方法传入的 intent 对象,然后 Android 系统会尝试再次重新创建该 Service,并执行 onStartCommand 回调方法,但是 onStartCommand 回调方法的 Intent 参数为 null,也就是 onStartCommand 方法虽然会执行但是获取不到 intent 信息。如果你的 Service 可以在任意时刻运行或结束都没什么问题,而且不需要 intent 信息,那么就可以在 onStartCommand 方法中返回 START_STICKY,比如一个用来播放背景音乐功能的 Service 就适合返回该值。

START_REDELIVER_INTENT

如果返回 START_REDELIVER_INTENT,表示 Service 运行的进程被 Android 系统强制杀掉之后,与返回 START_STICKY 的情况类似,Android 系统会将再次重新创建该 Service,并执行 onStartCommand 回调方法,但是不同的是,Android 系统会再次将 Service 在被杀掉之前最后一次传入 onStartCommand 方法中的 Intent 再次保留下来并再次传入到重新创建后的 Service 的 onStartCommand 方法中,这样我们就能读取到 intent 参数。只要返回 START_REDELIVER_INTENT,那么 onStartCommand 重的 intent 一定不是 null。如果我们的 Service 需要依赖具体的 Intent 才能运行(需要从 Intent 中读取相关数据信息等),并且在强制销毁后有必要重新创建运行,那么这样的 Service 就适合返回 START_REDELIVER_INTENT。

onBind

Service 中的 onBind 方法是抽象方法,所以 Service 类本身就是抽象类,也就是 onBind 方法是必须重写的,即使我们用不到。在通过 startService 使用 Service 时,我们在重写 onBind 方法时,只需要将其返回 null 即可。onBind 方法主要是用于给 bindService 方法调用 Service 时才会使用到。

onDestroy

onDestroy: 通过 startService 方法启动的 Service 会无限期运行,只有当调用了 Context 的 stopService 或在 Service 内部调用 stopSelf 方法时,Service 才会停止运行并销毁,在销毁的时候会执行 Service 回调函数。

2.3、bindService 生命周期


bindService 方式启动 Service 主要有以下几个生命周期函数:

onCreate():

首次创建服务时,系统将调用此方法。如果服务已在运行,则不会调用此方法,该方法只调用一次。

onStartCommand():

当另一个组件通过调用 startService()请求启动服务时,系统将调用此方法。

onDestroy():

当服务不再使用且将被销毁时,系统将调用此方法。

onBind():

当另一个组件通过调用 bindService()与服务绑定时,系统将调用此方法。

onUnbind():

当另一个组件通过调用 unbindService()与服务解绑时,系统将调用此方法。

onRebind():

当旧的组件与服务解绑后,另一个新的组件与服务绑定,onUnbind()返回 true 时,系统将调用此方法。

3、fragemnt

3.1、创建方式

(1)静态创建

首先我们需要创建一个 xml 文件,然后创建与之对应的 java 文件,通过 onCreatView()的返回方法进行关联,最后我们需要在 Activity 中进行配置相关参数即在 Activity 的 xml 文件中放上 fragment 的位置。

 <fragment
        android:name="xxx.BlankFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    </fragment>
(2)动态创建

动态创建 Fragment 主要有以下几个步骤:

  1. 创建待添加的 fragment 实例。
  2. 获取 FragmentManager,在 Activity 中可以直接通过调用 getSupportFragmentManager()方法得到。
  3. 开启一个事务,通过调用 beginTransaction()方法开启。
  4. 向容器内添加或替换 fragment,一般使用 repalce()方法实现,需要传入容器的 id 和待添加的 fragment 实例。
  5. 提交事务,调用 commit()方法来完成。

3.2、Adapter 对比

FragmnetPageAdapter 在每次切换页面时,只是将 Fragment 进行分离,适合页面较少的 Fragment 使用以保存一些内存,对系统内存不会多大影响。

FragmentPageStateAdapter 在每次切换页面的时候,是将 Fragment 进行回收,适合页面较多的 Fragment 使用,这样就不会消耗更多的内存

3.3、Activity 生命周期

Activity 的生命周期如下图:

(1)动态加载:

动态加载时,Activity 的 onCreate()调用完,才开始加载 fragment 并调用其生命周期方法,所以在第一个生命周期方法 onAttach()中便能获取 Activity 以及 Activity 的布局的组件;

(2)静态加载:

1. 静态加载时,Activity 的 onCreate()调用过程中,fragment 也在加载,所以 fragment 无法获取到 Activity 的布局中的组件,但为什么能获取到 Activity 呢?

2. 原来在 fragment 调用 onAttach()之前其实还调用了一个方法 onInflate(),该方法被调用时 fragment 已经是和 Activity 相互结合了,所以可以获取到对方,但是 Activity 的 onCreate()调用还未完成,故无法获取 Activity 的组件;

3.Activity 的 onCreate()调用完成是,fragment 会调用 onActivityCreated()生命周期方法,因此在这儿开始便能获取到 Activity 的布局的组件;

3.4、与 Activity 通信

fragment 不通过构造函数进行传值的原因是因为横屏切换的时候获取不到值。

Activity 向 Fragment 传值:

Activity 向 Fragment 传值,要传的值放到 bundle 对象里;
在 Activity 中创建该 Fragment 的对象 fragment,通过调用 setArguments()传递到 fragment 中;
在该 Fragment 中通过调用 getArguments()得到 bundle 对象,就能得到里面的值。

Fragment 向 Activity 传值:
第一种:

在 Activity 中调用 getFragmentManager()得到 fragmentManager,,调用 findFragmentByTag(tag)或者通过 findFragmentById(id),例如:

FragmentManager fragmentManager = getFragmentManager();Fragment fragment = fragmentManager.findFragmentByTag(tag);
第二种:

通过回调的方式,定义一个接口(可以在 Fragment 类中定义),接口中有一个空的方法,在 fragment 中需要的时候调用接口的方法,值可以作为参数放在这个方法中,然后让 Activity 实现这个接口,必然会重写这个方法,这样值就传到了 Activity 中

Fragment 与 Fragment 之间是如何传值的:
第一种:

通过 findFragmentByTag 得到另一个的 Fragment 的对象,这样就可以调用另一个的方法了。

第二种:

通过接口回调的方式。

第三种:

通过 setArguments,getArguments 的方式。

3.5、api 区别

add

一种是 add 方式来进行 show 和 add,这种方式你切换 fragment 不会让 fragment 重新刷新,只会调用 onHiddenChanged(boolean isHidden)。

replace

而用 replace 方式会使 fragment 重新刷新,因为 add 方式是将 fragment 隐藏了而不是销毁再创建,replace 方式每次都是重新创建。

commit/commitAllowingStateLoss

两者都可以提交 fragment 的操作,唯一的不同是第二种方法,允许丢失一些界面的状态和信息,几乎所有的开发者都遇到过这样的错误:无法在 activity 调用了 onSaveInstanceState 之后再执行 commit(),这种异常时可以理解的,界面被系统回收(界面已经不存在),为了在下次打开的时候恢复原来的样子,系统为我们保存界面的所有状态,这个时候我们再去修改界面理论上肯定是不允许的,所以为了避免这种异常,要使用第二种方法。

3. 懒加载

我们经常在使用 fragment 时,常常会结合着 viewpager 使用,那么我们就会遇到一个问题,就是初始化 fragment 的时候,会连同我们写的网络请求一起执行,这样非常消耗性能,最理想的方式是,只有用户点开或滑动到当前 fragment 时,才进行请求网络的操作。因此,我们就产生了懒加载这样一个说法。

Viewpager 配合 fragment 使用,默认加载前两个 fragment。很容易造成网络丢包、阻塞等问题。

在 Fragment 中有一个 setUserVisibleHint 这个方法,而且这个方法是优于 onCreate()方法的,它会通过 isVisibleToUser 告诉我们当前 Fragment 我们是否可见,我们可以在可见的时候再进行网络加载。

从 log 上看 setUserVisibleHint()的调用早于 onCreateView,所以如果在 setUserVisibleHint()要实现懒加载的话,就必须要确保 View 以及其他变量都已经初始化结束,避免空指针。

使用步骤:

申明一个变量 isPrepare=false,isVisible=false, 标明当前页面是否被创建了
在 onViewCreated 周期内设置 isPrepare=true
在 setUserVisibleHint(boolean isVisible)判断是否显示,设置 isVisible=true
判断 isPrepare 和 isVisible,都为 true 开始加载数据,然后恢复 isPrepare 和 isVisible 为 false,防止重复加载。

关于 Android Fragment 的懒加载,可以参考下面的链接:Fragment 的懒加载

4、Activity

4.1、Activity 启动流程

用户从 Launcher 程序点击应用图标可启动应用的入口 Activity,Activity 启动时需要多个进程之间的交互,Android 系统中有一个 zygote 进程专用于孵化 Android 框架层和应用层程序的进程。还有一个 system_server 进程,该进程里运行了很多 binder service。例如 ActivityManagerService,PackageManagerService,WindowManagerService,这些 binder service 分别运行在不同的线程中,其中 ActivityManagerService 负责管理 Activity 栈,应用进程,task。

点击 Launcher 图标来启动 Activity

用户在 Launcher 程序里点击应用图标时,会通知 ActivityManagerService 启动应用的入口 Activity,ActivityManagerService 发现这个应用还未启动,则会通知 Zygote 进程孵化出应用进程,然后在这个 dalvik 应用进程里执行 ActivityThread 的 main 方法。应用进程接下来通知 ActivityManagerService 应用进程已启动,ActivityManagerService 保存应用进程的一个代理对象,这样 ActivityManagerService 可以通过这个代理对象控制应用进程,然后 ActivityManagerService 通知应用进程创建入口 Activity 的实例,并执行它的生命周期方法。

Android 绘制流程窗口启动流程分析

4.2、Activity 生命周期

(1)Activity 的形态

Active/Running:

Activity 处于活动状态,此时 Activity 处于栈顶,是可见状态,可与用户进行交互。

Paused:

当 Activity 失去焦点时,或被一个新的非全屏的 Activity,或被一个透明的 Activity 放置在栈顶时,Activity 就转化为 Paused 状态。但我们需要明白,此时 Activity 只是失去了与用户交互的能力,其所有的状态信息及其成员变量都还存在,只有在系统内存紧张的情况下,才有可能被系统回收掉。

Stopped:

当一个 Activity 被另一个 Activity 完全覆盖时,被覆盖的 Activity 就会进入 Stopped 状态,此时它不再可见,但是跟 Paused 状态一样保持着其所有状态信息及其成员变量。

Killed:

当 Activity 被系统回收掉时,Activity 就处于 Killed 状态。

Activity 会在以上四种形态中相互切换,至于如何切换,这因用户的操作不同而异。了解了 Activity 的 4 种形态后,我们就来聊聊 Activity 的生命周期。

Activity 的生命周期

所谓的典型的生命周期就是在有用户参与的情况下,Activity 经历从创建,运行,停止,销毁等正常的生命周期过程。

onCreate

该方法是在 Activity 被创建时回调,它是生命周期第一个调用的方法,我们在创建 Activity 时一般都需要重写该方法,然后在该方法中做一些初始化的操作,如通过 setContentView 设置界面布局的资源,初始化所需要的组件信息等。

onStart

此方法被回调时表示 Activity 正在启动,此时 Activity 已处于可见状态,只是还没有在前台显示,因此无法与用户进行交互。可以简单理解为 Activity 已显示而我们无法看见摆了。

onResume

当此方法回调时,则说明 Activity 已在前台可见,可与用户交互了(处于前面所说的 Active/Running 形态),onResume 方法与 onStart 的相同点是两者都表示 Activity 可见,只不过 onStart 回调时 Activity 还是后台无法与用户交互,而 onResume 则已显示在前台,可与用户交互。当然从流程图,我们也可以看出当 Activity 停止后(onPause 方法和 onStop 方法被调用),重新回到前台时也会调用 onResume 方法,因此我们也可以在 onResume 方法中初始化一些资源,比如重新初始化在 onPause 或者 onStop 方法中释放的资源。

onPause

此方法被回调时则表示 Activity 正在停止(Paused 形态),一般情况下 onStop 方法会紧接着被回调。但通过流程图我们还可以看到一种情况是 onPause 方法执行后直接执行了 onResume 方法,这属于比较极端的现象了,这可能是用户操作使当前 Activity 退居后台后又迅速地再回到到当前的 Activity,此时 onResume 方法就会被回调。当然,在 onPause 方法中我们可以做一些数据存储或者动画停止或者资源回收的操作,但是不能太耗时,因为这可能会影响到新的 Activity 的显示——onPause 方法执行完成后,新 Activity 的 onResume 方法才会被执行。

onStop

一般在 onPause 方法执行完成直接执行,表示 Activity 即将停止或者完全被覆盖(Stopped 形态),此时 Activity 不可见,仅在后台运行。同样地,在 onStop 方法可以做一些资源释放的操作(不能太耗时)。

onRestart

表示 Activity 正在重新启动,当 Activity 由不可见变为可见状态时,该方法被回调。这种情况一般是用户打开了一个新的 Activity 时,当前的 Activity 就会被暂停(onPause 和 onStop 被执行了),接着又回到当前 Activity 页面时,onRestart 方法就会被回调。

onDestroy

此时 Activity 正在被销毁,也是生命周期最后一个执行的方法,一般我们可以在此方法中做一些回收工作和最终的资源释放。

小结

到这里我们来个小结,当 Activity 启动时,依次会调用 onCreate(),onStart(),onResume(),而当 Activity 退居后台时(不可见,点击 Home 或者被新的 Activity 完全覆盖),onPause()和 onStop()会依次被调用。当 Activity 重新回到前台(从桌面回到原 Activity 或者被覆盖后又回到原 Activity)时,onRestart(),onStart(),onResume()会依次被调用。当 Activity 退出销毁时(点击 back 键),onPause(),onStop(),onDestroy()会依次被调用,到此 Activity 的整个生命周期方法回调完成。现在我们再回头看看之前的流程图,应该是相当清晰了吧。嗯,这就是 Activity 整个典型的生命周期过程。

2、View 部分知识点

Android 的 Activity、PhoneWindow 和 DecorView 的关系可以用下面的图表示:

2.1、DecorView 浅析

例如,有下面一个视图,DecorView 为整个 Window 界面的最顶层 View,它只有一个子元素 LinearLayout。代表整个 Window 界面,包含通知栏、标题栏、内容显示栏三块区域。其中 LinearLayout 中有两个 FrameLayout 子元素。

DecorView 的作用

DecorView 是顶级 View,本质是一个 FrameLayout 它包含两部分,标题栏和内容栏,都是 FrameLayout。内容栏 id 是 content,也就是 activity 中设置 setContentView 的部分,最终将布局添加到 id 为 content 的 FrameLayout 中。
获取 content:ViewGroup content=findViewById(android.id.content)
获取设置的 View:getChildAt(0).

使用总结

每个 Activity 都包含一个 Window 对象,Window 对象通常是由 PhoneWindow 实现的。
PhoneWindow:将 DecorView 设置为整个应用窗口的根 View,是 Window 的实现类。它是 Android 中的最基本的窗口系统,每个 Activity 均会创建一个 PhoneWindow 对象,是 Activity 和整个 View 系统交互的接口。
DecorView:是顶层视图,将要显示的具体内容呈现在 PhoneWindow 上,DecorView 是当前 Activity 所有 View 的祖先,它并不会向用户呈现任何东西。

2.2、View 的事件分发

View 的事件分发机制可以使用下图表示:

如上图,图分为 3 层,从上往下依次是 Activity、ViewGroup、View。

  1. 事件从左上角那个白色箭头开始,由 Activity 的 dispatchTouchEvent 做分发
  2. 箭头的上面字代表方法返回值,(return true、return false、return super.xxxxx(),super
    的意思是调用父类实现。
  3. dispatchTouchEvent 和 onTouchEvent 的框里有个【true—-> 消费】的字,表示的意思是如果方法返回 true,那么代表事件就此消费,不会继续往别的地方传了,事件终止。
  4. 目前所有的图的事件是针对 ACTION_DOWN 的,对于 ACTION_MOVE 和 ACTION_UP 我们最后做分析。
  5. 之前图中的 Activity 的 dispatchTouchEvent 有误(图已修复),只有 return
    super.dispatchTouchEvent(ev) 才是往下走,返回 true 或者 false 事件就被消费了(终止传递)。

ViewGroup 事件分发

当一个点击事件产生后,它的传递过程将遵循如下顺序:

Activity -> Window -> View

事件总是会传递给 Activity,之后 Activity 再传递给 Window,最后 Window 再传递给顶级的 View,顶级的 View 在接收到事件后就会按照事件分发机制去分发事件。如果一个 View 的 onTouchEvent 返回了 FALSE,那么它的父容器的 onTouchEvent 将会被调用,依次类推,如果所有都不处理这个事件的话,那么 Activity 将会处理这个事件。

对于 ViewGroup 的事件分发过程,大概是这样的:如果顶级的 ViewGroup 拦截事件即 onInterceptTouchEvent 返回 true 的话,则事件会交给 ViewGroup 处理,如果 ViewGroup 的 onTouchListener 被设置的话,则 onTouch 将会被调用,否则的话 onTouchEvent 将会被调用,也就是说:两者都设置的话,onTouch 将会屏蔽掉 onTouchEvent,在 onTouchEvent 中,如果设置了 onClickerListener 的话,那么 onClick 将会被调用。如果顶级 ViewGroup 不拦截的话,那么事件将会被传递给它所在的点击事件的子 view,这时候子 view 的 dispatchTouchEvent 将会被调用

View 的事件分发

dispatchTouchEvent -> onTouch(setOnTouchListener) -> onTouchEvent -> onClick

onTouch 和 onTouchEvent 的区别
两者都是在 dispatchTouchEvent 中调用的,onTouch 优先于 onTouchEvent,如果 onTouch 返回 true,那么 onTouchEvent 则不执行,及 onClick 也不执行。

2.3、View 的绘制

在 xml 布局文件中,我们的 layout_width 和 layout_height 参数可以不用写具体的尺寸,而是 wrap_content 或者是 match_parent。这两个设置并没有指定真正的大小,可是我们绘制到屏幕上的 View 必须是要有具体的宽高的,正是因为这个原因,我们必须自己去处理和设置尺寸。当然了,View 类给了默认的处理,但是如果 View 类的默认处理不满足我们的要求,我们就得重写 onMeasure 函数啦~。

onMeasure 函数是一个 int 整数,里面放了测量模式和尺寸大小。int 型数据占用 32 个 bit,而 google 实现的是,将 int 数据的前面 2 个 bit 用于区分不同的布局模式,后面 30 个 bit 存放的是尺寸的数据。
onMeasure 函数的使用如下图:

MeasureSpec 有三种测量模式:

match_parent—>EXACTLY。怎么理解呢?match_parent 就是要利用父 View 给我们提供的所有剩余空间,而父 View 剩余空间是确定的,也就是这个测量模式的整数里面存放的尺寸。

wrap_content—>AT_MOST。怎么理解:就是我们想要将大小设置为包裹我们的 view 内容,那么尺寸大小就是父 View 给我们作为参考的尺寸,只要不超过这个尺寸就可以啦,具体尺寸就根据我们的需求去设定。

固定尺寸(如 100dp)—>EXACTLY。用户自己指定了尺寸大小,我们就不用再去干涉了,当然是以指定的大小为主啦。


2.4、ViewGroup 的绘制

自定义 ViewGroup 可就没那么简单啦~,因为它不仅要管好自己的,还要兼顾它的子 View。我们都知道 ViewGroup 是个 View 容器,它装纳 child View 并且负责把 child View 放入指定的位置。

  1. 首先,我们得知道各个子 View 的大小吧,只有先知道子 View 的大小,我们才知道当前的 ViewGroup 该设置为多大去容纳它们。
  2. 根据子 View 的大小,以及我们的 ViewGroup 要实现的功能,决定出 ViewGroup 的大小
  3. ViewGroup 和子 View 的大小算出来了之后,接下来就是去摆放了吧,具体怎么去摆放呢?这得根据你定制的需求去摆放了,比如,你想让子 View 按照垂直顺序一个挨着一个放,或者是按照先后顺序一个叠一个去放,这是你自己决定的。
  4. 已经知道怎么去摆放还不行啊,决定了怎么摆放就是相当于把已有的空间”分割”成大大小小的空间,每个空间对应一个子 View,我们接下来就是把子 View 对号入座了,把它们放进它们该放的地方去。


自定义 ViewGroup 可以参考:Android 自定义 ViewGroup

3、系统原理

3.1、打包原理

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

具体说来:

  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 文件的速度会更快。

3.2、安装流程

Android apk 的安装过程主要氛围以下几步:

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

可以使用下面的图表示:

4、第三方库解析

4.1、Retrofit 网络请求框架

概念:Retrofit 是一个基于 RESTful 的 HTTP 网络请求框架的封装,其中网络请求的本质是由 OKHttp 完成的,而 Retrofit 仅仅负责网络请求接口的封装。

原理:App 应用程序通过 Retrofit 请求网络,实际上是使用 Retrofit 接口层封装请求参数,Header、URL 等信息,之后由 OKHttp 完成后续的请求,在服务器返回数据之后,OKHttp 将原始的结果交给 Retrofit,最后根据用户的需求对结果进行解析。

retrofit 使用

1. 在 retrofit 中通过一个接口作为 http 请求的 api 接口

public interface NetApi {@GET("repos/{owner}/{repo}/contributors")
    Call<ResponseBody> contributorsBySimpleGetCall(@Path("owner") String owner, @Path("repo") String repo);
}

2. 创建一个 Retrofit 实例

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

3. 调用 api 接口

NetApi repo = retrofit.create(NetApi.class);

// 第三步:调用网络请求的接口获取网络请求
retrofit2.Call<ResponseBody> call = repo.contributorsBySimpleGetCall("username", "path");
call.enqueue(new Callback<ResponseBody>() { // 进行异步请求
    @Override
    public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {// 进行异步操作}

    @Override
    public void onFailure(Call<ResponseBody> call, Throwable t) {// 执行错误回调方法}
});

retrofit 动态代理

retrofit 执行的原理如下:
1. 首先,通过 method 把它转换成 ServiceMethod。
2. 然后,通过 serviceMethod,args 获取到 okHttpCall 对象。
3. 最后,再把 okHttpCall 进一步封装并返回 Call 对象。
首先,创建 retrofit 对象的方法如下:

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

在创建 retrofit 对象的时候用到了 build()方法,该方法的实现如下:

public Retrofit build() {if (baseUrl == null) {throw new IllegalStateException("Base URL required.");
  }

  okhttp3.Call.Factory callFactory = this.callFactory;
  if (callFactory == null) {callFactory = new OkHttpClient(); // 设置 kHttpClient
  }

  Executor callbackExecutor = this.callbackExecutor;
  if (callbackExecutor == null) {callbackExecutor = platform.defaultCallbackExecutor(); // 设置默认回调执行器
  }

  // Make a defensive copy of the adapters and add the default Call adapter.
  List<CallAdapter.Factory> adapterFactories = new ArrayList<>(this.adapterFactories);
  adapterFactories.add(platform.defaultCallAdapterFactory(callbackExecutor));

  // Make a defensive copy of the converters.
  List<Converter.Factory> converterFactories = new ArrayList<>(this.converterFactories);

  return new Retrofit(callFactory, baseUrl, converterFactories, adapterFactories,
      callbackExecutor, validateEagerly); // 返回新建的 Retrofit 对象
}

该方法返回了一个 Retrofit 对象,通过 retrofit 对象创建网络请求的接口的方式如下:

NetApi repo = retrofit.create(NetApi.class);

retrofit 对象的 create()方法的实现如下:‘

public <T> T create(final Class<T> service) {Utils.validateServiceInterface(service);
  if (validateEagerly) {eagerlyValidateMethods(service);
  }
  return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service},
      new InvocationHandler() {private final Platform platform = Platform.get();

        @Override public Object invoke(Object proxy, Method method, Object... args)
            throws Throwable {
          // If the method is a method from Object then defer to normal invocation.
          if (method.getDeclaringClass() == Object.class) {return method.invoke(this, args); // 直接调用该方法
          }
          if (platform.isDefaultMethod(method)) {return platform.invokeDefaultMethod(method, service, proxy, args); // 通过平台对象调用该方法
          }
          ServiceMethod serviceMethod = loadServiceMethod(method); // 获取 ServiceMethod 对象
          OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args); // 传入参数生成 okHttpCall 对象
          return serviceMethod.callAdapter.adapt(okHttpCall); // 执行 okHttpCall
        }
      });
}

4.2、图片加载库对比

Picasso:120K

Glide:475K

Fresco:3.4M

Android-Universal-Image-Loader:162K

图片函数库的选择需要根据 APP 的具体情况而定,对于严重依赖图片缓存的 APP,例如壁纸类,图片社交类 APP 来说,可以选择最专业的 Fresco。对于一般的 APP,选择 Fresco 会显得比较重,毕竟 Fresco3.4M 的体量摆在这。根据 APP 对图片的显示和缓存的需求从低到高,我们可以对以上函数库做一个排序。

Picasso < Android-Universal-Image-Loader < Glide < Fresco

2. 介绍:

Picasso:和 Square 的网络库一起能发挥最大作用,因为 Picasso 可以选择将网络请求的缓存部分交给了 okhttp 实现。

Glide:模仿了 Picasso 的 API,而且在他的基础上加了很多的扩展(比如 gif 等支持),Glide 默认的 Bitmap 格式是 RGB_565,比 Picasso 默认的 ARGB_8888 格式的内存开销要小一半;Picasso 缓存的是全尺寸的(只缓存一种),而 Glide 缓存的是跟 ImageView 尺寸相同的(即 5656 和 128128 是两个缓存)。

FB 的图片加载框架 Fresco:最大的优势在于 5.0 以下 (最低 2.3) 的 bitmap 加载。在 5.0 以下系统,Fresco 将图片放到一个特别的内存区域(Ashmem 区)。当然,在图片不显示的时候,占用的内存会自动被释放。这会使得 APP 更加流畅,减少因图片内存占用而引发的 OOM。为什么说是 5.0 以下,因为在 5.0 以后系统默认就是存储在 Ashmem 区了。

3. 总结:

Picasso 所能实现的功能,Glide 都能做,无非是所需的设置不同。但是 Picasso 体积比起 Glide 小太多如果项目中网络请求本身用的就是 okhttp 或者 retrofit(本质还是 okhttp),那么建议用 Picasso,体积会小很多(Square 全家桶的干活)。Glide 的好处是大型的图片流,比如 gif、Video,如果你们是做美拍、爱拍这种视频类应用,建议使用。

Fresco 在 5.0 以下的内存优化非常好,代价就是体积也非常的大,按体积算 Fresco>Glide>Picasso

不过在使用起来也有些不便(小建议:他只能用内置的一个 ImageView 来实现这些功能,用起来比较麻烦,我们通常是根据 Fresco 自己改改,直接使用他的 Bitmap 层)

4.3、各种 json 解析库使用

参考链接:https://www.cnblogs.com/kunpe…

(1)Google 的 Gson

Gson 是目前功能最全的 Json 解析神器,Gson 当初是为因应 Google 公司内部需求而由 Google 自行研发而来,但自从在 2008 年五月公开发布第一版后已被许多公司或用户应用。Gson 的应用主要为 toJson 与 fromJson 两个转换函数,无依赖,不需要例外额外的 jar,能够直接跑在 JDK 上。而在使用这种对象转换之前需先创建好对象的类型以及其成员才能成功的将 JSON 字符串成功转换成相对应的对象。类里面只要有 get 和 set 方法,Gson 完全可以将复杂类型的 json 到 bean 或 bean 到 json 的转换,是 JSON 解析的神器。Gson 在功能上面无可挑剔,但是性能上面比 FastJson 有所差距。

(2)阿里巴巴的 FastJson

Fastjson 是一个 Java 语言编写的高性能的 JSON 处理器, 由阿里巴巴公司开发。

无依赖,不需要例外额外的 jar,能够直接跑在 JDK 上。FastJson 在复杂类型的 Bean 转换 Json 上会出现一些问题,可能会出现引用的类型,导致 Json 转换出错,需要制定引用。FastJson 采用独创的算法,将 parse 的速度提升到极致,超过所有 json 库。

综上 Json 技术的比较,在项目选型的时候可以使用 Google 的 Gson 和阿里巴巴的 FastJson 两种并行使用,如果只是功能要求,没有性能要求,可以使用 google 的 Gson,如果有性能上面的要求可以使用 Gson 将 bean 转换 json 确保数据的正确,使用 FastJson 将 Json 转换 Bean

5、热点技术

参考链接 - Android 组件化方案

5.1、组件化

(1)概念:

组件化:是将一个 APP 分成多个 module,每个 module 都是一个组件,也可以是一个基础库供组件依赖,开发中可以单独调试部分组件,组件中不需要相互依赖但是可以相互调用,最终发布的时候所有组件以 lib 的形式被主 APP 工程依赖打包成一个 apk。

(2)由来:

  1. APP 版本迭代,新功能不断增加,业务变得复杂,维护成本高
  2. 业务耦合度高,代码臃肿,团队内部多人协作开发困难
  3. Android 编译代码卡顿,单一工程下代码耦合严重,修改一处需要重新编译打包,耗时耗力。
  4. 方便单元测试,单独改一个业务模块,不需要着重关注其他模块。

(3)优势:

  1. 组件化将通用模块独立出来,统一管理,以提高复用,将页面拆分为粒度更小的组件,组件内部出了包含 UI 实现,还可以包含数据层和逻辑层
  2. 每个组件度可以独立编译、加快编译速度、独立打包。
  3. 每个工程内部的修改,不会影响其他工程。
  4. 业务库工程可以快速拆分出来,集成到其他 App 中。
  5. 迭代频繁的业务模块采用组件方式,业务线研发可以互不干扰、提升协作效率,并控制产品质量,加强稳定性。
  6. 并行开发,团队成员只关注自己的开发的小模块,降低耦合性,后期维护方便等。

(4)考虑问题:

模式切换:如何使得 APP 在单独调试跟整体调试自由切换

组件化后的每一个业务的 module 都可以是一个单独的 APP(isModuleRun=false),release 包的时候各个业务 module 作为 lib 依赖,这里完全由一个变量控制,在根项目 gradle.properties 里面 isModuleRun=true。isModuleRun 状态不同,加载 application 和 AndroidManifest 都不一样,以此来区分是独立的 APK 还是 lib。

在 build.grade 里面配置:

资源冲突

当我们创建了多个 Module 的时候,如何解决相同资源文件名合并的冲突,业务 Module 和 BaseModule 资源文件名称重复会产生冲突,解决方案在于:

每个 module 都有 app_name,为了不让资源名重名,在每个组件的 build.gradle 中增加 resourcePrefix“xxx_强行检查资源名称前缀。固定每个组件的资源前缀。但是 resourcePrefix 这个值只能限定 xml 里面的资源,并不能限定图片资源。

依赖关系

多个 Module 之间如何引用一些共同的 library 以及工具类

组件通信

组件化之后,Module 之间是相互隔离的,如何进行 UI 跳转以及方法调用,具体可以使用阿里巴巴 ARouter 或者美团的 WMRouter 等路由框架。

各业务 Module 之前不需要任何依赖可以通过路由跳转,完美解决业务之间耦合。

入口参数

我们知道组件之间是有联系的,所以在单独调试的时候如何拿到其它的 Module 传递过来的参数

Application

当组件单独运行的时候,每个 Module 自成一个 APK,那么就意味着会有多个 Application,很显然我们不愿意重复写这么多代码,所以我们只需要定义一个 BaseApplication 即可,其它的 Application 直接继承此 BaseApplication 就 OK 了,BaseApplication 里面还可定义公用的参数。

关于如何进行组件化,可以参考:安居客 Android 项目架构演进

5.2、插件化

参考链接 - 插件化入门

(1)概述

提到插件化,就不得不提起方法数超过 65535 的问题,我们可以通过 Dex 分包来解决,同时也可以通过使用插件化开发来解决。插件化的概念就是由宿主 APP 去加载以及运行插件 APP。

(2 优点)

在一个大的项目里面,为了明确的分工,往往不同的团队负责不同的插件 APP,这样分工更加明确。各个模块封装成不同的插件 APK,不同模块可以单独编译,提高了开发效率。
解决了上述的方法数超过限制的问题。可以通过上线新的插件来解决线上的 BUG,达到“热修复”的效果。
减小了宿主 APK 的体积。

(3 缺点)

插件化开发的 APP 不能在 Google Play 上线,也就是没有海外市场。

6、屏幕适配

6.1、基本概念

屏幕尺寸

含义:手机对角线的物理尺寸 单位:英寸(inch),1 英寸 =2.54cm

Android 手机常见的尺寸有 5 寸、5.5 寸、6 寸,6.5 寸等等

屏幕分辨率

含义:手机在横向、纵向上的像素点数总和

一般描述成屏幕的”宽 x 高”=AxB 含义:屏幕在横向方向(宽度)上有 A 个像素点,在纵向方向

(高)有 B 个像素点 例子:1080×1920,即宽度方向上有 1080 个像素点,在高度方向上有 1920 个像素点

单位:px(pixel),1px= 1 像素点

UI 设计师的设计图会以 px 作为统一的计量单位

Android 手机常见的分辨率:320×480、480×800、720×1280、1080×1920

屏幕像素密度

含义:每英寸的像素点数 单位:dpi(dots per ich)

假设设备内每英寸有 160 个像素,那么该设备的屏幕像素密度 =160dpi

6.2、适配方法

1. 支持各种屏幕尺寸:使用 wrap_content, match_parent, weight. 要确保布局的灵活性并适应各种尺寸的屏幕,应使用“wrap_content”、“match_parent”控制某些视图组件的宽度和高度。

2. 使用相对布局,禁用绝对布局。

3. 使用 LinearLayout 的 weight 属性

假如我们的宽度不是 0dp(wrap_content 和 0dp 的效果相同),则是 match_parent 呢?

android:layout_weight 的真实含义是: 如果 View 设置了该属性并且有效,那么该 View 的宽度等于原有宽度 (android:layout_width) 加上剩余空间的占比。

从这个角度我们来解释一下上面的现象。在上面的代码中,我们设置每个 Button 的宽度都是 match_parent,假设屏幕宽度为 L,那么每个 Button 的宽度也应该都为 L,剩余宽度就等于 L -(L+L)= -L。

Button1 的 weight=1,剩余宽度占比为 1 /(1+2)= 1/3,所以最终宽度为 L +1/3*(-L)=2/3L,Button2 的计算类似,最终宽度为 L +2/3(-L)=1/3L。

4. 使用.9 图片

6.3、今日头条屏幕适配

参考链接:今日头条屏幕适配方案终极版

7、性能优化

参考链接:Android 性能监测工具,优化内存、卡顿、耗电、APK 大小的方法
Android 的性能优化,主要是从以下几个方面进行优化的:
稳定(内存溢出、崩溃)
流畅(卡顿)
耗损(耗电、流量)
安装包(APK 瘦身)
影响稳定性的原因很多,比如内存使用不合理、代码异常场景考虑不周全、代码逻辑不合理等,都会对应用的稳定性造成影响。其中最常见的两个场景是:Crash 和 ANR,这两个错误将会使得程序无法使用。所以做好 Crash 全局监控,处理闪退同时把崩溃信息、异常信息收集记录起来,以便后续分析; 合理使用主线程处理业务,不要在主线程中做耗时操作,防止 ANR 程序无响应发生。

(一)稳定——内存优化

(1)Memory Monitor 工具:

它是 Android Studio 自带的一个内存监视工具,它可以很好地帮助我们进行内存实时分析。通过点击 Android Studio 右下角的 Memory Monitor 标签,打开工具可以看见较浅蓝色代表 free 的内存,而深色的部分代表使用的内存从内存变换的走势图变换,可以判断关于内存的使用状态,例如当内存持续增高时,可能发生内存泄漏;当内存突然减少时,可能发生 GC 等,如下图所示。

LeakCanary 工具:
LeakCanary 是 Square 公司基于 MAT 开发的一款监控 Android 内存泄漏的开源框架。其工作的原理是:
监测机制利用了 Java 的 WeakReference 和 ReferenceQueue,通过将 Activity 包装到 WeakReference 中,被 WeakReference 包装过的 Activity 对象如果被回收,该 WeakReference 引用会被放到 ReferenceQueue 中,通过监测 ReferenceQueue 里面的内容就能检查到 Activity 是否能够被回收(在 ReferenceQueue 中说明可以被回收,不存在泄漏;否则,可能存在泄漏,LeakCanary 是执行一遍 GC,若还未在 ReferenceQueue 中,就会认定为泄漏)。

如果 Activity 被认定为泄露了,就抓取内存 dump 文件 (Debug.dumpHprofData);之后通过 HeapAnalyzerService.runAnalysis 进行分析内存文件分析;接着通过 HeapAnalyzer (checkForLeak—findLeakingReference—findLeakTrace) 来进行内存泄漏分析。最后通过 DisplayLeakService 进行内存泄漏的展示。

(3)Android Lint 工具:

Android Lint Tool 是 Android Sutido 种集成的一个 Android 代码提示工具,它可以给你布局、代码提供非常强大的帮助。硬编码会提示以级别警告,例如:在布局文件中写了三层冗余的 LinearLayout 布局、直接在 TextView 中写要显示的文字、字体大小使用 dp 而不是 sp 为单位,就会在编辑器右边看到提示。

(二)流畅——卡顿优化

卡顿的场景通常是发生在用户交互体验最直接的方面。影响卡顿的两大因素,分别是界面绘制和数据处理。

界面绘制:主要原因是绘制的层级深、页面复杂、刷新不合理,由于这些原因导致卡顿的场景更多出现在 UI 和启动后的初始界面以及跳转到页面的绘制上。

数据处理:导致这种卡顿场景的原因是数据处理量太大,一般分为三种情况,一是数据在处理 UI 线程,二是数据处理占用 CPU 高,导致主线程拿不到时间片,三是内存增加导致 GC 频繁,从而引起卡顿。

(1)布局优化

在 Android 种系统对 View 进行测量、布局和绘制时,都是通过对 View 数的遍历来进行操作的。如果一个 View 数的高度太高就会严重影响测量、布局和绘制的速度。Google 也在其 API 文档中建议 View 高度不宜哦过 10 层。现在版本种 Google 使用 RelativeLayout 替代 LineraLayout 作为默认根布局,目的就是降低 LineraLayout 嵌套产生布局树的高度,从而提高 UI 渲染的效率。

布局复用,使用 <include> 标签重用 layout;
提高显示速度,使用 <ViewStub> 延迟 View 加载;
减少层级,使用 <merge> 标签替换父级布局;
注意使用 wrap_content,会增加 measure 计算成本;
删除控件中无用属性;

(2)绘制优化

过度绘制是指在屏幕上的某个像素在同一帧的时间内被绘制了多次。在多层次重叠的 UI 结构中,如果不可见的 UI 也在做绘制的操作,就会导致某些像素区域被绘制了多次,从而浪费了多余的 CPU 以及 GPU 资源。如何避免过度绘制?

布局上的优化。移除 XML 中非必须的背景,移除 Window 默认的背景、按需显示占位背景图片

自定义 View 优化。使用 canvas.clipRect() 帮助系统识别那些可见的区域,只有在这个区域内才会被绘制。

(3)启动优化

应用一般都有闪屏页 SplashActivity,优化闪屏页的 UI 布局,可以通过 Profile GPU Rendering 检测丢帧情况。

(三)节省——耗电优化

在 Android5.0 以前,关于应用电量消耗的测试即麻烦又不准确,而 5.0 之后 Google 专门引入了一个获取设备上电量消耗信息的 API—— Battery Historian。Battery Historian 是一款由 Google 提供的 Android 系统电量分析工具,直观地展示出手机的电量消耗过程,通过输入电量分析文件,显示消耗情况。

最后提供一些可供参考耗电优化的方法:

(1)计算优化。算法、for 循环优化、Switch..case 替代 if..else、避开浮点运算。

浮点运算:计算机里整数和小数形式就是按普通格式进行存储,例如 1024、3.1415926 等等,这个没什么特点,但是这样的数精度不高,表达也不够全面,为了能够有一种数的通用表示法,就发明了浮点数。浮点数的表示形式有点像科学计数法(.×10^),它的表示形式是 0.×10^,在计算机中的形式为 .* e ±),其中前面的星号代表定点小数,也就是整数部分为 0 的纯小数,后面的指数部分是定点整数。利用这样的形式就能表示出任意一个整数和小数,例如 1024 就能表示成 0.1024×10^4,也就是 .1024e+004,3.1415926 就能表示成 0.31415926×10^1,也就是 .31415926e+001,这就是浮点数。浮点数进行的运算就是浮点运算。浮点运算比常规运算更复杂,因此计算机进行浮点运算速度要比进行常规运算慢得多。

(2)避免 Wake Lock 使用不当。

Wake Lock 是一种锁的机制,主要是相对系统的休眠而言的,, 只要有人拿着这个锁,系统就无法进入休眠意思就是我的程序给 CPU 加了这个锁那系统就不会休眠了,这样做的目的是为了全力配合我们程序的运行。有的情况如果不这么做就会出现一些问题,比如微信等及时通讯的心跳包会在熄屏不久后停止网络访问等问题。所以微信里面是有大量使用到了 Wake_Lock 锁。系统为了节省电量,CPU 在没有任务忙的时候就会自动进入休眠。有任务需要唤醒 CPU 高效执行的时候,就会给 CPU 加 Wake_Lock 锁。大家经常犯的错误,我们很容易去唤醒 CPU 来工作,但是很容易忘记释放 Wake_Lock。

(3)使用 Job Scheduler 管理后台任务。

在 Android 5.0 API 21 中,google 提供了一个叫做 JobScheduler API 的组件,来处理当某个时间点或者当满足某个特定的条件时执行一个任务的场景,例如当用户在夜间休息时或设备接通电源适配器连接 WiFi 启动下载更新的任务。这样可以在减少资源消耗的同时提升应用的效率。

(四)安装包——APK 瘦身

(1)安装包的组成结构

assets 文件夹。存放一些配置文件、资源文件,assets 不会自动生成对应的 ID,而是通过 AssetManager 类的接口获取。

res。res 是 resource 的缩写,这个目录存放资源文件,会自动生成对应的 ID 并映射到 .R 文件中,访问直接使用资源 ID。

META-INF。保存应用的签名信息,签名信息可以验证 APK 文件的完整性。

AndroidManifest.xml。这个文件用来描述 Android 应用的配置信息,一些组件的注册信息、可使用权限等。

classes.dex。Dalvik 字节码程序,让 Dalvik 虚拟机可执行,一般情况下,Android 应用在打包时通过 Android SDK 中的 dx 工具将 Java 字节码转换为 Dalvik 字节码。

resources.arsc。记录着资源文件和资源 ID 之间的映射关系,用来根据资源 ID 寻找资源。

(2)减少安装包大小

代码混淆。使用 IDE 自带的 proGuard 代码混淆器工具,它包括压缩、优化、混淆等功能。
资源优化。比如使用 Android Lint 删除冗余资源,资源文件最少化等。
图片优化。比如利用 PNG 优化工具 对图片做压缩处理。推荐目前最先进的压缩工具 Googlek 开源库 zopfli。如果应用在 0 版本以上,推荐使用 WebP 图片格式。
避免重复或无用功能的第三方库。例如,百度地图接入基础地图即可、讯飞语音无需接入离线、图片库 GlidePicasso 等。
插件化开发。比如功能模块放在服务器上,按需下载,可以减少安装包大小。
可以使用微信开源资源文件混淆工具——AndResGuard。一般可以压缩 apk 的 1M 左右大。

7.1、冷启动与热启动

参考链接:https://www.jianshu.com/p/03c…

冷启动
在启动应用时,系统中没有该应用的进程,这时系统会创建一个新的进程分配给该应用;

热启动
在启动应用时,系统中已有该应用的进程(例:按 back 键、home 键,应用虽然会退出,但是该应用的进程还是保留在后台);

区别
冷启动:系统没有该应用的进程,需要创建一个新的进程分配给应用,所以会先创建和初始化 Application 类,再创建和初始化 MainActivity 类(包括一系列的测量、布局、绘制),最后显示在界面上。热启动:从已有的进程中来启动,不会创建和初始化 Application 类,直接创建和初始化 MainActivity 类(包括一系列的测量、布局、绘制),最后显示在界面上。

冷启动流程
Zygote 进程中 fork 创建出一个新的进程;创建和初始化 Application 类、创建 MainActivity;inflate 布局、当 onCreate/onStart/onResume 方法都走完;contentView 的 measure/layout/draw 显示在界面上。

冷启动优化
减少在 Application 和第一个 Activity 的 onCreate()方法的工作量;不要让 Application 参与业务的操作;不要在 Application 进行耗时操作;不要以静态变量的方式在 Application 中保存数据;减少布局的复杂性和深度;

8、MVP 模式架构

8.1、MVP 模式

MVP 架构由 MVC 发展而来。在 MVP 中,M 代表 Model,V 代表 View,P 代表 Presenter。

模型层(Model): 主要是获取数据功能,业务逻辑和实体模型。

视图层(View):对应于 Activity 或 Fragment,负责视图的部分展示和业务逻辑用户交互

控制层(Presenter): 负责完成 View 层与 Model 层间的交互,通过 P 层来获取 M 层中数据后返回给 V 层,使得 V 层与 M 层间没有耦合。

在 MVP 中,Presenter 层完全将 View 层和 Model 层进行了分离,把主要程序逻辑放在 Presenter 层实现,Presenter 与具体的 View 层(Activity)是没有直接的关联,是通过定义接口来进行交互的,从而使得当 View 层(Activity)发生改变时,Persenter 依然可以保持不变。View 层接口类只应该只有 set/get 方法,及一些界面显示内容和用户输入,除此之外不应该有多余的内容。绝不允许 View 层直接访问 Model 层,这是与 MVC 最大区别之处,也是 MVP 核心优点。

9、虚拟机

9.1、Android Dalvik 虚拟机和 ART 虚拟机对比

Dalvik

Android4.4 及以前使用的都是 Dalvik 虚拟机,我们知道 Apk 在打包的过程中会先将 java 等源码通过 javac 编译成.class 文件,但是我们的 Dalvik 虚拟机只会执行.dex 文件,这个时候 dx 会将.class 文件转换成 Dalvik 虚拟机执行的.dex 文件。Dalvik 虚拟机在启动的时候会先将.dex 文件转换成快速运行的机器码,又因为 65535 这个问题,导致我们在应用冷启动的时候有一个合包的过程,最后导致的一个结果就是我们的 app 启动慢,这就是 Dalvik 虚拟机的 JIT 特性(Just In Time)。

ART

ART 虚拟机是在 Android5.0 才开始使用的 Android 虚拟机,ART 虚拟机必须要兼容 Dalvik 虚拟机的特性,但是 ART 有一个很好的特性 AOT(ahead of time),这个特性就是我们在安装 APK 的时候就将 dex 直接处理成可直接供 ART 虚拟机使用的机器码,ART 虚拟机将.dex 文件转换成可直接运行的.oat 文件,ART 虚拟机天生支持多 dex,所以也不会有一个合包的过程,所以 ART 虚拟机会很大的提升 APP 冷启动速度。

ART 优点:

加快 APP 冷启动速度

提升 GC 速度

提供功能全面的 Debug 特性

ART 缺点:

APP 安装速度慢,因为在 APK 安装的时候要生成可运行.oat 文件

APK 占用空间大,因为在 APK 安装的时候要生成可运行.oat 文件

arm 处理器

关于 ART 更详细的介绍,可以参考 Android ART 详解

总结

熟悉 Android 性能分析工具、UI 卡顿、APP 启动、包瘦身和内存性能优化

熟悉 Android APP 架构设计,模块化、组件化、插件化开发

熟练掌握 Java、设计模式、网络、多线程技术

Java 基本知识点

1、Java 的类加载过程

jvm 将.class 类文件信息加载到内存并解析成对应的 class 对象的过程,注意:jvm 并不是一开始就把所有的类加载进内存中,只是在第一次遇到某个需要运行的类才会加载,并且只加载一次

主要分为三部分:1、加载,2、链接(1. 验证,2. 准备,3. 解析),3、初始化

1:加载

类加载器包括 BootClassLoader、ExtClassLoader、APPClassLoader

2:链接

验证:(验证 class 文件的字节流是否符合 jvm 规范)

准备:为类变量分配内存,并且进行赋初值

解析:将常量池里面的符号引用(变量名)替换成直接引用(内存地址)过程,在解析阶段,jvm 会把所有的类名、方法名、字段名、这些符号引用替换成具体的内存地址或者偏移量。

3:初始化

主要对类变量进行初始化,执行类构造器的过程,换句话说,只对 static 修试的变量或者语句进行初始化。

范例:Person person = new Person(); 为例进行说明。

Java 编程思想中的类的初始化过程主要有以下几点:

  1. 找到 class 文件,将它加载到内存
  2. 在堆内存中分配内存地址
  3. 初始化
  4. 将堆内存地址指给栈内存中的 p 变量

2、String、StringBuilder、StringBuffer

StringBuffer 里面的很多方法添加了 synchronized 关键字,是可以表征线程安全的,所以多线程情况下使用它。

执行速度:

StringBuilder > StringBuffer > String

StringBuilder 牺牲了性能来换取速度的,这两个是可以直接在原对象上面进行修改,省去了创建新对象和回收老对象的过程,而 String 是字符串常量(final)修试,另外两个是字符串变量,常量对象一旦创建就不可以修改,变量是可以进行修改的,所以对于 String 字符串的操作包含下面三个步骤:

  1. 创建一个新对象,名字和原来的一样
  2. 在新对象上面进行修改
  3. 原对象被垃圾回收掉

3、JVM 内存结构

Java 对象实例化过程中,主要使用到虚拟机栈、Java 堆和方法区。Java 文件经过编译之后首先会被加载到 jvm 方法区中,jvm 方法区中很重的一个部分是运行时常量池,用以存储 class 文件类的版本、字段、方法、接口等描述信息和编译期间的常量和静态常量。

3.1、JVM 基本结构

类加载器 classLoader,在 JVM 启动时或者类运行时将需要的.class 文件加载到内存中。
执行引擎,负责执行 class 文件中包含的字节码指令。
本地方法接口,主要是调用 C /C++ 实现的本地方法及返回结果。
内存区域(运行时数据区),是在 JVM 运行的时候操作所分配的内存区,
主要分为以下五个部分,如下图:

  • 方法区:用于存储类结构信息的地方,包括常量池、静态变量、构造函数等。
  • Java 堆(heap):存储 Java 实例或者对象的地方。这块是 gc 的主要区域。
  • Java 栈(stack):Java 栈总是和线程关联的,每当创建一个线程时,JVM 就会为这个线程创建一个对应的 Java 栈。在这个 java 栈中又会包含多个栈帧,每运行一个方法就创建一个栈帧,用于存储局部变量表、操作栈、方法返回值等。每一个方法从调用直至执行完成的过程,就对应一个栈帧在 java 栈中入栈到出栈的过程。所以 java 栈是线程私有的。
  • 程序计数器:用于保存当前线程执行的内存地址,由于 JVM 是多线程执行的,所以为了保证线程切换回来后还能恢复到原先状态,就需要一个独立的计数器,记录之前中断的地方,可见程序计数器也是线程私有的。
  • 本地方法栈:和 Java 栈的作用差不多,只不过是为 JVM 使用到的 native 方法服务的。

3.2、JVM 源码分析

https://www.jianshu.com/nb/12…

4、GC 机制

垃圾收集器一般完成两件事

  1. 检测出垃圾;
  2. 回收垃圾;

4.1 Java 对象引用

通常,Java 对象的引用可以分为 4 类:强引用、软引用、弱引用和虚引用。
强引用:通常可以认为是通过 new 出来的对象,即使内存不足,GC 进行垃圾收集的时候也不会主动回收。

Object obj = new Object();

软引用:在内存不足的时候,GC 进行垃圾收集的时候会被 GC 回收。

Object obj = new Object();
SoftReference<Object> softReference = new SoftReference<>(obj);

弱引用:无论内存是否充足,GC 进行垃圾收集的时候都会回收。


Object obj = new Object();
WeakReference<Object> weakReference = new WeakReference<>(obj);

虚引用:和弱引用类似,主要区别在于虚引用必须和引用队列一起使用。

Object obj = new Object();
ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
PhantomReference<Object> phantomReference = new PhantomReference<>(obj, referenceQueue);

引用队列:如果软引用和弱引用被 GC 回收,JVM 就会把这个引用加到引用队列里,如果是虚引用,在回收前就会被加到引用队列里。

垃圾检测方法:

引用计数法 :给每个对象添加引用计数器,每个地方引用它,计数器就 +1,失效时 -1。如果两个对象互相引用时,就导致无法回收。
可达性分析算法:以根集对象为起始点进行搜索,如果对象不可达的话就是垃圾对象。根集(Java 栈中引用的对象、方法区中常量池中引用的对象、本地方法中引用的对象等。JVM 在垃圾回收的时候,会检查堆中所有对象是否被这些根集对象引用,不能够被引用的对象就会被垃圾回收器回收。)

垃圾回收算法:

常见的垃圾回收算法有:

标记 - 清除

标记:首先标记所有需要回收的对象,在标记完成之后统计回收所有被标记的对象,它的标记过程即为上面的可达性分析算法。
清除:清除所有被标记的对象
缺点:
效率不足,标记和清除效率都不高
空间问题,标记清除之后会产生大量不连续的内存碎片,导致大对象分配无法找到足够的空间,提前进行垃圾回收。

复制回收算法
将可用的内存按容量划分为大小相等的 2 块,每次只用一块,当这一块的内存用完了,就将存活的对象复制到另外一块上面,然后把已使用过的内存空间一次清理掉。

缺点:

将内存缩小了原本的一般,代价比较高
大部分对象是“朝生夕灭”的,所以不必按照 1:1 的比例划分。
现在商业虚拟机采用这种算法回收新生代,但不是按 1:1 的比例,而是将内存区域划分为 eden 空间、from 空间、to 空间 3 个部分。
其中 from 空间和 to 空间可以视为用于复制的两块大小相同、地位相等,且可进行角色互换的空间块。from 和 to 空间也称为 survivor 空间,即幸存者空间,用于存放未被回收的对象。

在垃圾回收时,eden 空间中的存活对象会被复制到未使用的 survivor 空间中 (假设是 to),正在使用的 survivor 空间 (假设是 from) 中的年轻对象也会被复制到 to 空间中 (大对象,或者老年对象会直接进入老年带,如果 to 空间已满,则对象也会直接进入老年代)。此时,eden 空间和 from 空间中的剩余对象就是垃圾对象,可以直接清空,to 空间则存放此次回收后的存活对象。这种改进的复制算法既保证了空间的连续性,又避免了大量的内存空间浪费。

标记 - 整理

在老年代的对象大都是存活对象,复制算法在对象存活率教高的时候,效率就会变得比较低。根据老年代的特点,有人提出了“标记 - 压缩算法(Mark-Compact)”

标记过程与标记 - 清除的标记一样,但后续不是对可回收对象进行清理,而是让所有的对象都向一端移动,然后直接清理掉端边界以外的内存。

这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。

分带收集算法

根据对象存活的周期不同将内存划分为几块,一般是把 Java 堆分为老年代和新生代,这样根据各个年代的特点采用适当的收集算法。

新生代每次收集都有大量对象死去,只有少量存活,那就选用复制算法,复制的对象数较少就可完成收集。
老年代对象存活率高,使用标记 - 压缩算法,以提高垃圾回收效率。

5、类加载器

程序在启动的时候,并不会一次性加载程序所要用的所有 class 文件,而是根据程序的需要,通过 Java 的类加载机制(ClassLoader)来动态加载某个 class 文件到内存当中的,从而只有 class 文件被载入到了内存之后,才能被其它 class 所引用。所以 ClassLoader 就是用来动态加载 class 文件到内存当中用的。

5.1、双亲委派原理

每个 ClassLoader 实例都有一个父类加载器的引用(不是继承关系,是一个包含的关系),虚拟机内置的类加载器(Bootstrap ClassLoader)本身没有父类加载器,但是可以用做其他 ClassLoader 实例的父类加载器。

当一个 ClassLoader 实例需要加载某个类时,它会试图在亲自搜索这个类之前先把这个任务委托给它的父类加载器,这个过程是由上而下依次检查的,首先由顶层的类加载器 Bootstrap CLassLoader 进行加载,如果没有加载到,则把任务转交给 Extension CLassLoader 视图加载,如果也没有找到,则转交给 AppCLassLoader 进行加载,还是没有的话,则交给委托的发起者,由它到指定的文件系统或者网络等 URL 中进行加载类。还没有找到的话,则会抛出 CLassNotFoundException 异常。否则将这个类生成一个类的定义,并将它加载到内存中,最后返回这个类在内存中的 Class 实例对象。

5.2、为什么使用双亲委托模型

JVM 在判断两个 class 是否相同时,不仅要判断两个类名是否相同,还要判断是否是同一个类加载器加载的。

避免重复加载,父类已经加载了,则子 CLassLoader 没有必要再次加载。
考虑安全因素,假设自定义一个 String 类,除非改变 JDK 中 CLassLoader 的搜索类的默认算法,否则用户自定义的 CLassLoader 如法加载一个自己写的 String 类,因为 String 类在启动时就被引导类加载器 Bootstrap CLassLoader 加载了。

关于 Android 的双亲委托机制,可以参考 android classloader 双亲委托模式

6、集合

Java 集合类主要由两个接口派生出:Collection 和 Map,这两个接口是 Java 集合的根接口。

Collection 接口是集合类的根接口,Java 中没有提供这个接口的直接的实现类。但是却让其被继承产生了两个接口,就是 Set 和 List。Set 中不能包含重复的元素。List 是一个有序的集合,可以包含重复的元素,提供了按索引访问的方式。

Map 是 Java.util 包中的另一个接口,它和 Collection 接口没有关系,是相互独立的,但是都属于集合类的一部分。Map 包含了 key-value 对。Map 不能包含重复的 key,但是可以包含相同的 value。

6.1、区别

List,Set 都是继承自 Collection 接口,Map 则不是;
List 特点:元素有放入顺序,元素可重复; Set 特点:元素无放入顺序,元素不可重复,重复元素会覆盖掉,(注意:元素虽然无放入顺序,但是元素在 set 中的位置是有该元素的 HashCode 决定的,其位置其实是固定的,加入 Set 的 Object 必须定义 equals()方法;
LinkedList、ArrayList、HashSet 是非线程安全的,Vector 是线程安全的;
HashMap 是非线程安全的,HashTable 是线程安全的;

6.2、List 和 Vector 比较

Vector 是多线程安全的,线程安全就是说多线程访问同一代码,不会产生不确定的结果。而 ArrayList 不是,这个可以从源码中看出,Vector 类中的方法很多有 synchronized 进行修饰,这样就导致了 Vector 在效率上无法与 ArrayList 相比;
两个都是采用的线性连续空间存储元素,但是当空间不足的时候,两个类的增加方式是不同。
Vector 可以设置增长因子,而 ArrayList 不可以。
Vector 是一种老的动态数组,是线程同步的,效率很低,一般不赞成使用。

6.3、HashSet 如何保证不重复

HashSet 底层通过 HashMap 来实现的,在往 HashSet 中添加元素是

public boolean add(E e) {return map.put(e, PRESENT)==null;
}


// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();

在 HashMap 中进行查找是否存在这个 key,value 始终是一样的,主要有以下几种情况:

  • 如果 hash 码值不相同,说明是一个新元素,存;
  • 如果 hash 码值相同,且 equles 判断相等,说明元素已经存在,不存;
  • 如果 hash 码值相同,且 equles 判断不相等,说明元素不存在,存;
  • 如果有元素和传入对象的 hash 值相等,那么,继续进行 equles()判断,如果仍然相等,那么就认为传入元素已经存在,不再添加,结束,否则仍然添加;

6.4、HashSet 与 Treeset 的适用场景

  • HashSet 是基于 Hash 算法实现的,其性能通常都优于 TreeSet。为快速查找而设计的 Set,我们通常都应该使用 HashSet,在我们需要排序的功能时,我们才使用 TreeSet。
  • TreeSet 是二叉树(红黑树的树据结构)实现的,Treeset 中的数据是自动排好序的,不允许放入 null 值
  • HashSet 是哈希表实现的,HashSet 中的数据是无序的,可以放入 null,但只能放入一个 null,两者中的值都不能重复,就如数据库中唯一约束。
  • HashSet 是基于 Hash 算法实现的,其性能通常都优于 TreeSet。为快速查找而设计的 Set,我们通常都应该使用 HashSet,在我们需要排序的功能时,我们才使用 TreeSet。

6.5、HashMap 与 TreeMap、HashTable 的区别及适用场景

HashMap 非线程安全,基于哈希表 (散列表) 实现。使用 HashMap 要求添加的键类明确定义了 hashCode()和 equals()[可以重写 hashCode()和 equals()],为了优化 HashMap 空间的使用,您可以调优初始容量和负载因子。其中散列表的冲突处理主要分两种,一种是开放定址法,另一种是链表法。HashMap 的实现中采用的是链表法。
TreeMap:非线程安全基于红黑树实现,TreeMap 没有调优选项,因为该树总处于平衡状态

7、常量池

7.1、Interger 中的 128(-128~127)

当数值范围为 -128~127 时:如果两个 new 出来 Integer 对象,即使值相同,通过“==”比较结果为 false,但两个对象直接赋值,则通过“==”比较结果为“true,这一点与 String 非常相似。
当数值不在 -128~127 时,无论通过哪种方式,即使两个对象的值相等,通过“==”比较,其结果为 false;
当一个 Integer 对象直接与一个 int 基本数据类型通过“==”比较,其结果与第一点相同;
Integer 对象的 hash 值为数值本身;

@Override
public int hashCode() {return Integer.hashCode(value);
}

7.2、为什么是 -128-127?

在 Integer 类中有一个静态内部类 IntegerCache,在 IntegerCache 类中有一个 Integer 数组,用以缓存当数值范围为 -128~127 时的 Integer 对象。

8、泛型

泛型是 Java SE 1.5 的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。Java 语言引入泛型的好处是安全简单。

泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率。

它提供了编译期的类型安全,确保你只能把正确类型的对象放入 集合中,避免了在运行时出现 ClassCastException。

使用 Java 的泛型时应注意以下几点:

  • 泛型的类型参数只能是类类型(包括自定义类),不能是简单类型。
  • 同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。
  • 泛型的类型参数可以有多个。
  • 泛型的参数类型可以使用 extends 语句,例如 <T extends superclass>。习惯上称为“有界类型”。
  • 泛型的参数类型还可以是通配符类型。例如 Class<?> classType =
    Class.forName(“java.lang.String”);

8.1 T 泛型和通配符泛型

    • ?表示不确定的 java 类型。
    • T 表示 java 类型。
    • K V 分别代表 java 键值中的 Key Value。
    • E 代表 Element。

    8.2 泛型擦除

    Java 中的泛型基本上都是在编译器这个层次来实现的。在生成的 Java 字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会在编译器在编译的时候去掉。这个过程就称为类型擦除。

    泛型是通过类型擦除来实现的,编译器在编译时擦除了所有类型相关的信息,所以在运行时不存在任何类型相关的信息。例如 List<String> 在运行时仅用一个 List 来表示。这样做的目的,是确保能和 Java 5 之前的版本开发二进制类库进行兼容。你无法在运行时访问到类型参数,因为编译器已经把泛型类型转换成了原始类型。

    8.3 限定通配符

    限定通配符对类型进行了限制。

    一种是 <? extends T> 它通过确保类型必须是 T 的子类来设定类型的上界,
    另一种是 <? super T> 它通过确保类型必须是 T 的父类来设定类型的下界。
    另一方面 <?> 表 示了非限定通配符,因为 <?> 可以用任意类型来替代。
    例如 List<? extends Number> 可以接受 List<Integer> 或 List<Float>。

    8.4 泛型面试题

    你可以把 List<String> 传递给一个接受 List<Object> 参数的方法吗?

    对任何一个不太熟悉泛型的人来说,这个 Java 泛型题目看起来令人疑惑,因为乍看起来 String 是一种 Object,所以 List<String> 应当可以用在需要 List<Object> 的地方,但是事实并非如此。真这样做的话会导致编译错误。如 果你再深一步考虑,你会发现 Java 这样做是有意义的,因为 List<Object> 可以存储任何类型的对象包括 String, Integer 等等,而 List<String> 却只能用来存储 Strings。

    Array 中可以用泛型吗?

    Array 事实上并不支持泛型,这也是为什么 Joshua Bloch 在 Effective Java 一书中建议使用 List 来代替 Array,因为 List 可以提供编译期的类型安全保证,而 Array 却不能。

    Java 中 List<Object> 和原始类型 List 之间的区别?

    原始类型和带参数类型 <Object> 之间的主要区别是,在编译时编译器不会对原始类型进行类型安全检查,却会对带参数的类型进行检查,通过使用 Object 作为类型,可以告知编译器该方法可以接受任何类型的对象,比如 String 或 Integer。这道题的考察点在于对泛型中原始类型的正确理解。它们之间的第二点区别是,你可以把任何带参数的类型传递给原始类型 List,但却不能把 List<String> 传递给接受 List<Object> 的方法,因为会产生编译错误。

    List<?> 是一个未知类型的 List,而 List<Object> 其实是任意类型的 List。你可以把 List<String>, List<Integer> 赋值给 List<?>,却不能把 List<String> 赋值给 List<Object>。

    9、反射

    9.1、概念

    JAVA 反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法;这种动态获取的信息以及动态调用对象的方法的功能称为 java 语言的反射机制。

    9.2、作用

    Java 反射机制主要提供了以下功能:在运行时判断任意一个对象所属的类;在运行时构造任意一个类的对象;在运行时判断任意一个类所具有的成员变量和方法;在运行时调用任意一个对象的方法;生成动态代理。

    10、代理

    代理这个词大家肯定已经非常熟悉,因为现实中接触的很多,其实现实中的东西恰恰可以非常形象和直观地反映出模式的抽象过程以及本质。现在房子不是吵得热火朝天吗?我们就以房子为例,来拨开代理的面纱。

    假设你有一套房子要卖,一种方法是你直接去网上发布出售信息,然后直接带要买房子的人来看房子、过户等一直到房子卖出去,但是可能你很忙,你没有时间去处理这些事情,所以你可以去找中介,让中介帮你处理这些琐碎事情,中介实际上就是你的代理。本来是你要做的事情,现在中介帮助你一一处理,对于买方来说跟你直接交易跟同中介直接交易没有任何差异,买方甚至可能觉察不到你的存在,这实际上就是代理的一个最大好处。

    接下来我们再深入考虑一下为什么你不直接买房子而需要中介?其实一个问题恰恰解答了什么时候该用代理模式的问题。

    原因一:你可能在外地上班,买房子的人没法找到你直接交易。

    对应到我们程序设计的时候就是:客户端无法直接操作实际对象。那么为什么无法直接操作?一种情况是你需要调用的对象在另外一台机器上,你需要跨越网络才能访问,如果让你直接 coding 去调用,你需要处理网络连接、处理打包、解包等等非常复杂的步骤,所以为了简化客户端的处理,我们使用代理模式,在客户端建立一个远程对象的代理,客户端就象调用本地对象一样调用该代理,再由代理去跟实际对象联系,对于客户端来说可能根本没有感觉到调用的东西在网络另外一端,这实际上就是 Web Service 的工作原理。另一种情况虽然你所要调用的对象就在本地,但是由于调用非常耗时,你怕影响你正常的操作,所以特意找个代理来处理这种耗时情况,一个最容易理解的就是 Word 里面装了很大一张图片,在 word 被打开的时候我们肯定要加载里面的内容一起打开,但是如果等加载完这个大图片再打开 Word 用户等得可能早已经跳脚了,所以我们可以为这个图片设置一个代理,让代理慢慢打开这个图片而不影响 Word 本来的打开的功能。申明一下我只是猜可能 Word 是这么做的,具体到底怎么做的,俺也不知道。

    原因二:你不知道怎么办过户手续,或者说除了你现在会干的事情外,还需要做其他的事情才能达成目的。

    对应到我们程序设计的时候就是:除了当前类能够提供的功能外,我们还需要补充一些其他功能。最容易想到的情况就是权限过滤,我有一个类做某项业务,但是由于安全原因只有某些用户才可以调用这个类,此时我们就可以做一个该类的代理类,要求所有请求必须通过该代理类,由该代理类做权限判断,如果安全则调用实际类的业务开始处理。可能有人说为什么我要多加个代理类?我只需要在原来类的方法里面加上权限过滤不就完了吗?在程序设计中有一个类的单一性原则问题,这个原则很简单,就是每个类的功能尽可能单一。为什么要单一,因为只有功能单一这个类被改动的可能性才会最小,就拿刚才的例子来说,如果你将权限判断放在当前类里面,当前这个类就既要负责自己本身业务逻辑、又要负责权限判断,那么就有两个导致该类变化的原因,现在如果权限规则一旦变化,这个类就必需得改,显然这不是一个好的设计。

    好了,原理的东西已经讲得差不多了,要是再讲个没完可能大家要扔砖头了。呵呵,接下来就看看怎么来实现代理。

    数据结构与算法

    https://zhuanlan.zhihu.com/p/…

    http://crazyandcoder.tech/201…

    1、排序

    排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。

    1.1、直接插入排序

    思想:

    将第一个数和第二个数排序,然后构成一个有序序列
    将第三个数插入进去,构成一个新的有序序列。
    对第四个数、第五个数……直到最后一个数,重复第二步。
    代码:

    首先设定插入次数,即循环次数,for(int i=1;i<length;i++),1 个数的那次不用插入。
    设定插入数和得到已经排好序列的最后一个数的位数。insertNum 和 j =i-1。

    2、设计模式

    参考:Android 开发中的一些设计模式

    2.1、单例设计模式

    单例主要分为:懒汉式单例、饿汉式单例、登记式单例。

    特点:

    1. 单例类只有一个实例
    2. 单例类必须自己创建自己的唯一实例
    3. 单例类必须给所有其他对象提供这一实例。

    在计算机系统中,像线程池,缓存、日志对象、对话框、打印机等常被设计成单例。

    懒汉式单例:

    Singleton 通过将构造方法限定为 private 避免了类在外部被实例化,在同一个虚拟机范围内,Singleton 的唯一实例只能通过 getInstance()方法访问。(事实上,通过 Java 反射机制是能够实例化构造方法为 private 的类的,那基本上会使所有的 Java 单例实现失效。

    它是线程不安全的,并发情况下很有可能出现多个 Singleton 实例,要实现线程安全,有以下三种方式:
    1. 在 getInstance 方法上加上同步

    2. 双重检查锁定

    3. 静态内部类

    这种方式对比前两种,既实现了线程安全,又避免了同步带来的性能影响。

    饿汉式单例:

    饿汉式在创建类的同时就已经创建好了一个静态的对象供系统使用,以后不再改变,所以天生是系统安全。

    其他

    https://juejin.im/post/5a3717…
    https://github.com/Mr-YangChe…

    Android 基本知识点
    1、常规知识点
    1、Android 类加载器
    在 Android 开发中,不管是插件化还是组件化,都是基于 Android 系统的类加载器 ClassLoader 来设计的。只不过 Android 平台上虚拟机运行的是 Dex 字节码,一种对 class 文件优化的产物,传统 Class 文件是一个 Java 源码文件会生成一个.class 文件,而 Android 是把所有 Class 文件进行合并、优化,然后再生成一个最终的 class.dex,目的是把不同 class 文件重复的东西只需保留一份,在早期的 Android 应用开发中,如果不对 Android 应用进行分 dex 处理,那么最后一个应用的 apk 只会有一个 dex 文件。

    Android 中常用的类加载器有两种,DexClassLoader 和 PathClassLoader,它们都继承于 BaseDexClassLoader。区别在于调用父类构造器时,DexClassLoader 多传了一个 optimizedDirectory 参数,这个目录必须是内部存储路径,用来缓存系统创建的 Dex 文件。而 PathClassLoader 该参数为 null,只能加载内部存储目录的 Dex 文件。所以我们可以用 DexClassLoader 去加载外部的 apk 文件,这也是很多插件化技术的基础。
    在这里插入图片描述

    2、Service
    理解 Android 的 Service,可以从以下几个方面来理解:

    Service 是在 main Thread 中执行,Service 中不能执行耗时操作(网络请求,拷贝数据库,大文件)。
    可以在 xml 中设置 Service 所在的进程,让 Service 在另外的进程中执行。
    Service 执行的操作最多是 20s,BroadcastReceiver 是 10s,Activity 是 5s。
    Activity 通过 bindService(Intent,ServiceConnection,flag)与 Service 绑定。
    Activity 可以通过 startService 和 bindService 启动 Service。
    IntentService
    IntentService 是一个抽象类,继承自 Service,内部存在一个 ServiceHandler(Handler)和 HandlerThread(Thread)。IntentService 是处理异步请求的一个类,在 IntentService 中有一个工作线程(HandlerThread)来处理耗时操作,启动 IntentService 的方式和普通的一样,不过当执行完任务之后,IntentService 会自动停止。另外可以多次启动 IntentService,每一个耗时操作都会以工作队列的形式在 IntentService 的 onHandleIntent 回调中执行,并且每次执行一个工作线程。IntentService 的本质是:封装了一个 HandlerThread 和 Handler 的异步框架。

    2.1、生命周期示意图
    Service 作为 Android 四大组件之一,应用非常广泛。和 Activity 一样,Service 也有一系列的生命周期回调函数,具体如下图。
    在这里插入图片描述

    通常,启动 Service 有两种方式,startService 和 bindService 方式。

    2.2、startService 生命周期
    当我们通过调用了 Context 的 startService 方法后,我们便启动了 Service,通过 startService 方法启动的 Service 会一直无限期地运行下去,只有在外部调用 Context 的 stopService 或 Service 内部调用 Service 的 stopSelf 方法时,该 Service 才会停止运行并销毁。

    onCreate
    onCreate: 执行 startService 方法时,如果 Service 没有运行的时候会创建该 Service 并执行 Service 的 onCreate 回调方法;如果 Service 已经处于运行中,那么执行 startService 方法不会执行 Service 的 onCreate 方法。也就是说如果多次执行了 Context 的 startService 方法启动 Service,Service 方法的 onCreate 方法只会在第一次创建 Service 的时候调用一次,以后均不会再次调用。我们可以在 onCreate 方法中完成一些 Service 初始化相关的操作。

    onStartCommand
    onStartCommand: 在执行了 startService 方法之后,有可能会调用 Service 的 onCreate 方法,在这之后一定会执行 Service 的 onStartCommand 回调方法。也就是说,如果多次执行了 Context 的 startService 方法,那么 Service 的 onStartCommand 方法也会相应的多次调用。onStartCommand 方法很重要,我们在该方法中根据传入的 Intent 参数进行实际的操作,比如会在此处创建一个线程用于下载数据或播放音乐等。

    public @StartResult int onStartCommand(Intent intent, @StartArgFlags int flags, int startId) {
    }
    当 Android 面临内存匮乏的时候,可能会销毁掉你当前运行的 Service,然后待内存充足的时候可以重新创建 Service,Service 被 Android 系统强制销毁并再次重建的行为依赖于 Service 中 onStartCommand 方法的返回值。我们常用的返回值有三种值,START_NOT_STICKY、START_STICKY 和 START_REDELIVER_INTENT,这三个值都是 Service 中的静态常量。

    START_NOT_STICKY
    如果返回 START_NOT_STICKY,表示当 Service 运行的进程被 Android 系统强制杀掉之后,不会重新创建该 Service,当然如果在其被杀掉之后一段时间又调用了 startService,那么该 Service 又将被实例化。那什么情境下返回该值比较恰当呢?如果我们某个 Service 执行的工作被中断几次无关紧要或者对 Android 内存紧张的情况下需要被杀掉且不会立即重新创建这种行为也可接受,那么我们便可将 onStartCommand 的返回值设置为 START_NOT_STICKY。举个例子,某个 Service 需要定时从服务器获取最新数据:通过一个定时器每隔指定的 N 分钟让定时器启动 Service 去获取服务端的最新数据。当执行到 Service 的 onStartCommand 时,在该方法内再规划一个 N 分钟后的定时器用于再次启动该 Service 并开辟一个新的线程去执行网络操作。假设 Service 在从服务器获取最新数据的过程中被 Android 系统强制杀掉,Service 不会再重新创建,这也没关系,因为再过 N 分钟定时器就会再次启动该 Service 并重新获取数据。

    START_STICKY
    如果返回 START_STICKY,表示 Service 运行的进程被 Android 系统强制杀掉之后,Android 系统会将该 Service 依然设置为 started 状态(即运行状态),但是不再保存 onStartCommand 方法传入的 intent 对象,然后 Android 系统会尝试再次重新创建该 Service,并执行 onStartCommand 回调方法,但是 onStartCommand 回调方法的 Intent 参数为 null,也就是 onStartCommand 方法虽然会执行但是获取不到 intent 信息。如果你的 Service 可以在任意时刻运行或结束都没什么问题,而且不需要 intent 信息,那么就可以在 onStartCommand 方法中返回 START_STICKY,比如一个用来播放背景音乐功能的 Service 就适合返回该值。

    START_REDELIVER_INTENT
    如果返回 START_REDELIVER_INTENT,表示 Service 运行的进程被 Android 系统强制杀掉之后,与返回 START_STICKY 的情况类似,Android 系统会将再次重新创建该 Service,并执行 onStartCommand 回调方法,但是不同的是,Android 系统会再次将 Service 在被杀掉之前最后一次传入 onStartCommand 方法中的 Intent 再次保留下来并再次传入到重新创建后的 Service 的 onStartCommand 方法中,这样我们就能读取到 intent 参数。只要返回 START_REDELIVER_INTENT,那么 onStartCommand 重的 intent 一定不是 null。如果我们的 Service 需要依赖具体的 Intent 才能运行(需要从 Intent 中读取相关数据信息等),并且在强制销毁后有必要重新创建运行,那么这样的 Service 就适合返回 START_REDELIVER_INTENT。

    onBind
    Service 中的 onBind 方法是抽象方法,所以 Service 类本身就是抽象类,也就是 onBind 方法是必须重写的,即使我们用不到。在通过 startService 使用 Service 时,我们在重写 onBind 方法时,只需要将其返回 null 即可。onBind 方法主要是用于给 bindService 方法调用 Service 时才会使用到。

    onDestroy
    onDestroy: 通过 startService 方法启动的 Service 会无限期运行,只有当调用了 Context 的 stopService 或在 Service 内部调用 stopSelf 方法时,Service 才会停止运行并销毁,在销毁的时候会执行 Service 回调函数。

    2.3、bindService 生命周期
    在这里插入图片描述
    bindService 方式启动 Service 主要有以下几个生命周期函数:

    onCreate():
    首次创建服务时,系统将调用此方法。如果服务已在运行,则不会调用此方法,该方法只调用一次。

    onStartCommand():
    当另一个组件通过调用 startService()请求启动服务时,系统将调用此方法。

    onDestroy():
    当服务不再使用且将被销毁时,系统将调用此方法。

    onBind():
    当另一个组件通过调用 bindService()与服务绑定时,系统将调用此方法。

    onUnbind():
    当另一个组件通过调用 unbindService()与服务解绑时,系统将调用此方法。

    onRebind():
    当旧的组件与服务解绑后,另一个新的组件与服务绑定,onUnbind()返回 true 时,系统将调用此方法。

    3、fragemnt
    3.1、创建方式
    (1)静态创建
    首先我们需要创建一个 xml 文件,然后创建与之对应的 java 文件,通过 onCreatView()的返回方法进行关联,最后我们需要在 Activity 中进行配置相关参数即在 Activity 的 xml 文件中放上 fragment 的位置。

    <fragment

        android:name="xxx.BlankFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent">
    </fragment>
    

    (2)动态创建
    动态创建 Fragment 主要有以下几个步骤:

    创建待添加的 fragment 实例。
    获取 FragmentManager,在 Activity 中可以直接通过调用 getSupportFragmentManager()方法得到。
    开启一个事务,通过调用 beginTransaction()方法开启。
    向容器内添加或替换 fragment,一般使用 repalce()方法实现,需要传入容器的 id 和待添加的 fragment 实例。
    提交事务,调用 commit()方法来完成。
    3.2、Adapter 对比
    FragmnetPageAdapter 在每次切换页面时,只是将 Fragment 进行分离,适合页面较少的 Fragment 使用以保存一些内存,对系统内存不会多大影响。

    FragmentPageStateAdapter 在每次切换页面的时候,是将 Fragment 进行回收,适合页面较多的 Fragment 使用,这样就不会消耗更多的内存

    3.3、Activity 生命周期
    Activity 的生命周期如下图:
    在这里插入图片描述

    (1)动态加载:
    动态加载时,Activity 的 onCreate()调用完,才开始加载 fragment 并调用其生命周期方法,所以在第一个生命周期方法 onAttach()中便能获取 Activity 以及 Activity 的布局的组件;

    (2)静态加载:
    1. 静态加载时,Activity 的 onCreate()调用过程中,fragment 也在加载,所以 fragment 无法获取到 Activity 的布局中的组件,但为什么能获取到 Activity 呢?

    2. 原来在 fragment 调用 onAttach()之前其实还调用了一个方法 onInflate(),该方法被调用时 fragment 已经是和 Activity 相互结合了,所以可以获取到对方,但是 Activity 的 onCreate()调用还未完成,故无法获取 Activity 的组件;

    3.Activity 的 onCreate()调用完成是,fragment 会调用 onActivityCreated()生命周期方法,因此在这儿开始便能获取到 Activity 的布局的组件;

    3.4、与 Activity 通信
    fragment 不通过构造函数进行传值的原因是因为横屏切换的时候获取不到值。

    Activity 向 Fragment 传值:
    Activity 向 Fragment 传值,要传的值放到 bundle 对象里;
    在 Activity 中创建该 Fragment 的对象 fragment,通过调用 setArguments()传递到 fragment 中;
    在该 Fragment 中通过调用 getArguments()得到 bundle 对象,就能得到里面的值。

    Fragment 向 Activity 传值:
    第一种:
    在 Activity 中调用 getFragmentManager()得到 fragmentManager,,调用 findFragmentByTag(tag)或者通过 findFragmentById(id),例如:

    FragmentManager fragmentManager = getFragmentManager();

    Fragment fragment = fragmentManager.findFragmentByTag(tag);
    第二种:
    通过回调的方式,定义一个接口(可以在 Fragment 类中定义),接口中有一个空的方法,在 fragment 中需要的时候调用接口的方法,值可以作为参数放在这个方法中,然后让 Activity 实现这个接口,必然会重写这个方法,这样值就传到了 Activity 中

    Fragment 与 Fragment 之间是如何传值的:
    第一种:
    通过 findFragmentByTag 得到另一个的 Fragment 的对象,这样就可以调用另一个的方法了。

    第二种:
    通过接口回调的方式。

    第三种:
    通过 setArguments,getArguments 的方式。

    3.5、api 区别
    add
    一种是 add 方式来进行 show 和 add,这种方式你切换 fragment 不会让 fragment 重新刷新,只会调用 onHiddenChanged(boolean isHidden)。

    replace
    而用 replace 方式会使 fragment 重新刷新,因为 add 方式是将 fragment 隐藏了而不是销毁再创建,replace 方式每次都是重新创建。

    commit/commitAllowingStateLoss
    两者都可以提交 fragment 的操作,唯一的不同是第二种方法,允许丢失一些界面的状态和信息,几乎所有的开发者都遇到过这样的错误:无法在 activity 调用了 onSaveInstanceState 之后再执行 commit(),这种异常时可以理解的,界面被系统回收(界面已经不存在),为了在下次打开的时候恢复原来的样子,系统为我们保存界面的所有状态,这个时候我们再去修改界面理论上肯定是不允许的,所以为了避免这种异常,要使用第二种方法。

    3. 懒加载
    我们经常在使用 fragment 时,常常会结合着 viewpager 使用,那么我们就会遇到一个问题,就是初始化 fragment 的时候,会连同我们写的网络请求一起执行,这样非常消耗性能,最理想的方式是,只有用户点开或滑动到当前 fragment 时,才进行请求网络的操作。因此,我们就产生了懒加载这样一个说法。

    Viewpager 配合 fragment 使用,默认加载前两个 fragment。很容易造成网络丢包、阻塞等问题。

    在 Fragment 中有一个 setUserVisibleHint 这个方法,而且这个方法是优于 onCreate()方法的,它会通过 isVisibleToUser 告诉我们当前 Fragment 我们是否可见,我们可以在可见的时候再进行网络加载。

    从 log 上看 setUserVisibleHint()的调用早于 onCreateView,所以如果在 setUserVisibleHint()要实现懒加载的话,就必须要确保 View 以及其他变量都已经初始化结束,避免空指针。

    使用步骤:

    申明一个变量 isPrepare=false,isVisible=false, 标明当前页面是否被创建了
    在 onViewCreated 周期内设置 isPrepare=true
    在 setUserVisibleHint(boolean isVisible)判断是否显示,设置 isVisible=true
    判断 isPrepare 和 isVisible,都为 true 开始加载数据,然后恢复 isPrepare 和 isVisible 为 false,防止重复加载。

    关于 Android Fragment 的懒加载,可以参考下面的链接:Fragment 的懒加载

    4、Activity
    4.1、Activity 启动流程
    用户从 Launcher 程序点击应用图标可启动应用的入口 Activity,Activity 启动时需要多个进程之间的交互,Android 系统中有一个 zygote 进程专用于孵化 Android 框架层和应用层程序的进程。还有一个 system_server 进程,该进程里运行了很多 binder service。例如 ActivityManagerService,PackageManagerService,WindowManagerService,这些 binder service 分别运行在不同的线程中,其中 ActivityManagerService 负责管理 Activity 栈,应用进程,task。

    点击 Launcher 图标来启动 Activity
    用户在 Launcher 程序里点击应用图标时,会通知 ActivityManagerService 启动应用的入口 Activity,ActivityManagerService 发现这个应用还未启动,则会通知 Zygote 进程孵化出应用进程,然后在这个 dalvik 应用进程里执行 ActivityThread 的 main 方法。应用进程接下来通知 ActivityManagerService 应用进程已启动,ActivityManagerService 保存应用进程的一个代理对象,这样 ActivityManagerService 可以通过这个代理对象控制应用进程,然后 ActivityManagerService 通知应用进程创建入口 Activity 的实例,并执行它的生命周期方法。

    Android 绘制流程窗口启动流程分析

    4.2、Activity 生命周期
    在这里插入图片描述

    (1)Activity 的形态
    Active/Running:
    Activity 处于活动状态,此时 Activity 处于栈顶,是可见状态,可与用户进行交互。

    Paused:
    当 Activity 失去焦点时,或被一个新的非全屏的 Activity,或被一个透明的 Activity 放置在栈顶时,Activity 就转化为 Paused 状态。但我们需要明白,此时 Activity 只是失去了与用户交互的能力,其所有的状态信息及其成员变量都还存在,只有在系统内存紧张的情况下,才有可能被系统回收掉。

    Stopped:
    当一个 Activity 被另一个 Activity 完全覆盖时,被覆盖的 Activity 就会进入 Stopped 状态,此时它不再可见,但是跟 Paused 状态一样保持着其所有状态信息及其成员变量。

    Killed:
    当 Activity 被系统回收掉时,Activity 就处于 Killed 状态。

    Activity 会在以上四种形态中相互切换,至于如何切换,这因用户的操作不同而异。了解了 Activity 的 4 种形态后,我们就来聊聊 Activity 的生命周期。

    Activity 的生命周期
    所谓的典型的生命周期就是在有用户参与的情况下,Activity 经历从创建,运行,停止,销毁等正常的生命周期过程。

    onCreate
    该方法是在 Activity 被创建时回调,它是生命周期第一个调用的方法,我们在创建 Activity 时一般都需要重写该方法,然后在该方法中做一些初始化的操作,如通过 setContentView 设置界面布局的资源,初始化所需要的组件信息等。

    onStart
    此方法被回调时表示 Activity 正在启动,此时 Activity 已处于可见状态,只是还没有在前台显示,因此无法与用户进行交互。可以简单理解为 Activity 已显示而我们无法看见摆了。

    onResume
    当此方法回调时,则说明 Activity 已在前台可见,可与用户交互了(处于前面所说的 Active/Running 形态),onResume 方法与 onStart 的相同点是两者都表示 Activity 可见,只不过 onStart 回调时 Activity 还是后台无法与用户交互,而 onResume 则已显示在前台,可与用户交互。当然从流程图,我们也可以看出当 Activity 停止后(onPause 方法和 onStop 方法被调用),重新回到前台时也会调用 onResume 方法,因此我们也可以在 onResume 方法中初始化一些资源,比如重新初始化在 onPause 或者 onStop 方法中释放的资源。

    onPause
    此方法被回调时则表示 Activity 正在停止(Paused 形态),一般情况下 onStop 方法会紧接着被回调。但通过流程图我们还可以看到一种情况是 onPause 方法执行后直接执行了 onResume 方法,这属于比较极端的现象了,这可能是用户操作使当前 Activity 退居后台后又迅速地再回到到当前的 Activity,此时 onResume 方法就会被回调。当然,在 onPause 方法中我们可以做一些数据存储或者动画停止或者资源回收的操作,但是不能太耗时,因为这可能会影响到新的 Activity 的显示——onPause 方法执行完成后,新 Activity 的 onResume 方法才会被执行。

    onStop
    一般在 onPause 方法执行完成直接执行,表示 Activity 即将停止或者完全被覆盖(Stopped 形态),此时 Activity 不可见,仅在后台运行。同样地,在 onStop 方法可以做一些资源释放的操作(不能太耗时)。

    onRestart
    表示 Activity 正在重新启动,当 Activity 由不可见变为可见状态时,该方法被回调。这种情况一般是用户打开了一个新的 Activity 时,当前的 Activity 就会被暂停(onPause 和 onStop 被执行了),接着又回到当前 Activity 页面时,onRestart 方法就会被回调。

    onDestroy
    此时 Activity 正在被销毁,也是生命周期最后一个执行的方法,一般我们可以在此方法中做一些回收工作和最终的资源释放。

    小结
    到这里我们来个小结,当 Activity 启动时,依次会调用 onCreate(),onStart(),onResume(),而当 Activity 退居后台时(不可见,点击 Home 或者被新的 Activity 完全覆盖),onPause()和 onStop()会依次被调用。当 Activity 重新回到前台(从桌面回到原 Activity 或者被覆盖后又回到原 Activity)时,onRestart(),onStart(),onResume()会依次被调用。当 Activity 退出销毁时(点击 back 键),onPause(),onStop(),onDestroy()会依次被调用,到此 Activity 的整个生命周期方法回调完成。现在我们再回头看看之前的流程图,应该是相当清晰了吧。嗯,这就是 Activity 整个典型的生命周期过程。

    2、View 部分知识点
    Android 的 Activity、PhoneWindow 和 DecorView 的关系可以用下面的图表示:
    在这里插入图片描述

    2.1、DecorView 浅析
    例如,有下面一个视图,DecorView 为整个 Window 界面的最顶层 View,它只有一个子元素 LinearLayout。代表整个 Window 界面,包含通知栏、标题栏、内容显示栏三块区域。其中 LinearLayout 中有两个 FrameLayout 子元素。
    在这里插入图片描述

    DecorView 的作用
    DecorView 是顶级 View,本质是一个 FrameLayout 它包含两部分,标题栏和内容栏,都是 FrameLayout。内容栏 id 是 content,也就是 activity 中设置 setContentView 的部分,最终将布局添加到 id 为 content 的 FrameLayout 中。
    获取 content:ViewGroup content=findViewById(android.id.content)
    获取设置的 View:getChildAt(0).

    使用总结
    每个 Activity 都包含一个 Window 对象,Window 对象通常是由 PhoneWindow 实现的。
    PhoneWindow:将 DecorView 设置为整个应用窗口的根 View,是 Window 的实现类。它是 Android 中的最基本的窗口系统,每个 Activity 均会创建一个 PhoneWindow 对象,是 Activity 和整个 View 系统交互的接口。
    DecorView:是顶层视图,将要显示的具体内容呈现在 PhoneWindow 上,DecorView 是当前 Activity 所有 View 的祖先,它并不会向用户呈现任何东西。

    2.2、View 的事件分发
    View 的事件分发机制可以使用下图表示:
    在这里插入图片描述
    如上图,图分为 3 层,从上往下依次是 Activity、ViewGroup、View。

    事件从左上角那个白色箭头开始,由 Activity 的 dispatchTouchEvent 做分发
    箭头的上面字代表方法返回值,(return true、return false、return super.xxxxx(),super
    的意思是调用父类实现。
    dispatchTouchEvent 和 onTouchEvent 的框里有个【true—-> 消费】的字,表示的意思是如果方法返回 true,那么代表事件就此消费,不会继续往别的地方传了,事件终止。
    目前所有的图的事件是针对 ACTION_DOWN 的,对于 ACTION_MOVE 和 ACTION_UP 我们最后做分析。
    之前图中的 Activity 的 dispatchTouchEvent 有误(图已修复),只有 return
    super.dispatchTouchEvent(ev) 才是往下走,返回 true 或者 false 事件就被消费了(终止传递)。
    ViewGroup 事件分发
    当一个点击事件产生后,它的传递过程将遵循如下顺序:

    Activity -> Window -> View

    事件总是会传递给 Activity,之后 Activity 再传递给 Window,最后 Window 再传递给顶级的 View,顶级的 View 在接收到事件后就会按照事件分发机制去分发事件。如果一个 View 的 onTouchEvent 返回了 FALSE,那么它的父容器的 onTouchEvent 将会被调用,依次类推,如果所有都不处理这个事件的话,那么 Activity 将会处理这个事件。

    对于 ViewGroup 的事件分发过程,大概是这样的:如果顶级的 ViewGroup 拦截事件即 onInterceptTouchEvent 返回 true 的话,则事件会交给 ViewGroup 处理,如果 ViewGroup 的 onTouchListener 被设置的话,则 onTouch 将会被调用,否则的话 onTouchEvent 将会被调用,也就是说:两者都设置的话,onTouch 将会屏蔽掉 onTouchEvent,在 onTouchEvent 中,如果设置了 onClickerListener 的话,那么 onClick 将会被调用。如果顶级 ViewGroup 不拦截的话,那么事件将会被传递给它所在的点击事件的子 view,这时候子 view 的 dispatchTouchEvent 将会被调用

    View 的事件分发
    dispatchTouchEvent -> onTouch(setOnTouchListener) -> onTouchEvent -> onClick

    onTouch 和 onTouchEvent 的区别
    两者都是在 dispatchTouchEvent 中调用的,onTouch 优先于 onTouchEvent,如果 onTouch 返回 true,那么 onTouchEvent 则不执行,及 onClick 也不执行。

    2.3、View 的绘制
    在 xml 布局文件中,我们的 layout_width 和 layout_height 参数可以不用写具体的尺寸,而是 wrap_content 或者是 match_parent。这两个设置并没有指定真正的大小,可是我们绘制到屏幕上的 View 必须是要有具体的宽高的,正是因为这个原因,我们必须自己去处理和设置尺寸。当然了,View 类给了默认的处理,但是如果 View 类的默认处理不满足我们的要求,我们就得重写 onMeasure 函数啦~。

    onMeasure 函数是一个 int 整数,里面放了测量模式和尺寸大小。int 型数据占用 32 个 bit,而 google 实现的是,将 int 数据的前面 2 个 bit 用于区分不同的布局模式,后面 30 个 bit 存放的是尺寸的数据。
    onMeasure 函数的使用如下图:
    在这里插入图片描述
    MeasureSpec 有三种测量模式:
    在这里插入图片描述

    match_parent—>EXACTLY。怎么理解呢?match_parent 就是要利用父 View 给我们提供的所有剩余空间,而父 View 剩余空间是确定的,也就是这个测量模式的整数里面存放的尺寸。

    wrap_content—>AT_MOST。怎么理解:就是我们想要将大小设置为包裹我们的 view 内容,那么尺寸大小就是父 View 给我们作为参考的尺寸,只要不超过这个尺寸就可以啦,具体尺寸就根据我们的需求去设定。

    固定尺寸(如 100dp)—>EXACTLY。用户自己指定了尺寸大小,我们就不用再去干涉了,当然是以指定的大小为主啦。

    在这里插入图片描述
    在这里插入图片描述

    2.4、ViewGroup 的绘制
    自定义 ViewGroup 可就没那么简单啦~,因为它不仅要管好自己的,还要兼顾它的子 View。我们都知道 ViewGroup 是个 View 容器,它装纳 child View 并且负责把 child View 放入指定的位置。

    首先,我们得知道各个子 View 的大小吧,只有先知道子 View 的大小,我们才知道当前的 ViewGroup 该设置为多大去容纳它们。

    根据子 View 的大小,以及我们的 ViewGroup 要实现的功能,决定出 ViewGroup 的大小

    ViewGroup 和子 View 的大小算出来了之后,接下来就是去摆放了吧,具体怎么去摆放呢?这得根据你定制的需求去摆放了,比如,你想让子 View 按照垂直顺序一个挨着一个放,或者是按照先后顺序一个叠一个去放,这是你自己决定的。

    已经知道怎么去摆放还不行啊,决定了怎么摆放就是相当于把已有的空间”分割”成大大小小的空间,每个空间对应一个子 View,我们接下来就是把子 View 对号入座了,把它们放进它们该放的地方去。
    在这里插入图片描述
    在这里插入图片描述

    自定义 ViewGroup 可以参考:Android 自定义 ViewGroup

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

    具体说来:

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

    3.2、安装流程
    Android apk 的安装过程主要氛围以下几步:

    复制 APK 到 /data/app 目录下,解压并扫描安装包。
    资源管理器解析 APK 里的资源文件。
    解析 AndroidManifest 文件,并在 /data/data/ 目录下创建对应的应用数据目录。
    然后对 dex 文件进行优化,并保存在 dalvik-cache 目录下。
    将 AndroidManifest 文件解析出的四大组件信息注册到 PackageManagerService 中。
    安装完成后,发送广播。
    可以使用下面的图表示:
    在这里插入图片描述

    4、第三方库解析
    4.1、Retrofit 网络请求框架
    概念:Retrofit 是一个基于 RESTful 的 HTTP 网络请求框架的封装,其中网络请求的本质是由 OKHttp 完成的,而 Retrofit 仅仅负责网络请求接口的封装。

    原理:App 应用程序通过 Retrofit 请求网络,实际上是使用 Retrofit 接口层封装请求参数,Header、URL 等信息,之后由 OKHttp 完成后续的请求,在服务器返回数据之后,OKHttp 将原始的结果交给 Retrofit,最后根据用户的需求对结果进行解析。

    retrofit 使用
    1. 在 retrofit 中通过一个接口作为 http 请求的 api 接口

    public interface NetApi {

    @GET("repos/{owner}/{repo}/contributors")
    Call<ResponseBody> contributorsBySimpleGetCall(@Path("owner") String owner, @Path("repo") String repo);

    }
    2. 创建一个 Retrofit 实例

    Retrofit retrofit = new Retrofit.Builder()

        .baseUrl("https://api.github.com/")
        .build();

    3. 调用 api 接口

    NetApi repo = retrofit.create(NetApi.class);

    // 第三步:调用网络请求的接口获取网络请求
    retrofit2.Call<ResponseBody> call = repo.contributorsBySimpleGetCall(“username”, “path”);
    call.enqueue(new Callback<ResponseBody>() {// 进行异步请求

    @Override
    public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {// 进行异步操作}
    
    @Override
    public void onFailure(Call<ResponseBody> call, Throwable t) {// 执行错误回调方法}

    });
    retrofit 动态代理
    retrofit 执行的原理如下:
    1. 首先,通过 method 把它转换成 ServiceMethod。
    2. 然后,通过 serviceMethod,args 获取到 okHttpCall 对象。
    3. 最后,再把 okHttpCall 进一步封装并返回 Call 对象。
    首先,创建 retrofit 对象的方法如下:

    Retrofit retrofit = new Retrofit.Builder()

        .baseUrl("https://api.github.com/")
        .build();

    在创建 retrofit 对象的时候用到了 build()方法,该方法的实现如下:

    public Retrofit build() {
    if (baseUrl == null) {

    throw new IllegalStateException("Base URL required.");

    }

    okhttp3.Call.Factory callFactory = this.callFactory;
    if (callFactory == null) {

    callFactory = new OkHttpClient(); // 设置 kHttpClient

    }

    Executor callbackExecutor = this.callbackExecutor;
    if (callbackExecutor == null) {

    callbackExecutor = platform.defaultCallbackExecutor(); // 设置默认回调执行器

    }

    // Make a defensive copy of the adapters and add the default Call adapter.
    List<CallAdapter.Factory> adapterFactories = new ArrayList<>(this.adapterFactories);
    adapterFactories.add(platform.defaultCallAdapterFactory(callbackExecutor));

    // Make a defensive copy of the converters.
    List<Converter.Factory> converterFactories = new ArrayList<>(this.converterFactories);

    return new Retrofit(callFactory, baseUrl, converterFactories, adapterFactories,

      callbackExecutor, validateEagerly); // 返回新建的 Retrofit 对象

    }
    该方法返回了一个 Retrofit 对象,通过 retrofit 对象创建网络请求的接口的方式如下:

    NetApi repo = retrofit.create(NetApi.class);
    retrofit 对象的 create()方法的实现如下:‘

    public <T> T create(final Class<T> service) {
    Utils.validateServiceInterface(service);
    if (validateEagerly) {

    eagerlyValidateMethods(service);

    }
    return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service},

      new InvocationHandler() {private final Platform platform = Platform.get();
    
        @Override public Object invoke(Object proxy, Method method, Object... args)
            throws Throwable {
          // If the method is a method from Object then defer to normal invocation.
          if (method.getDeclaringClass() == Object.class) {return method.invoke(this, args); // 直接调用该方法
          }
          if (platform.isDefaultMethod(method)) {return platform.invokeDefaultMethod(method, service, proxy, args); // 通过平台对象调用该方法
          }
          ServiceMethod serviceMethod = loadServiceMethod(method); // 获取 ServiceMethod 对象
          OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args); // 传入参数生成 okHttpCall 对象
          return serviceMethod.callAdapter.adapt(okHttpCall); // 执行 okHttpCall
        }
      });

    }
    4.2、图片加载库对比
    Picasso:120K

    Glide:475K

    Fresco:3.4M

    Android-Universal-Image-Loader:162K

    图片函数库的选择需要根据 APP 的具体情况而定,对于严重依赖图片缓存的 APP,例如壁纸类,图片社交类 APP 来说,可以选择最专业的 Fresco。对于一般的 APP,选择 Fresco 会显得比较重,毕竟 Fresco3.4M 的体量摆在这。根据 APP 对图片的显示和缓存的需求从低到高,我们可以对以上函数库做一个排序。

    Picasso < Android-Universal-Image-Loader < Glide < Fresco

    2. 介绍:
    Picasso:和 Square 的网络库一起能发挥最大作用,因为 Picasso 可以选择将网络请求的缓存部分交给了 okhttp 实现。

    Glide:模仿了 Picasso 的 API,而且在他的基础上加了很多的扩展(比如 gif 等支持),Glide 默认的 Bitmap 格式是 RGB_565,比 Picasso 默认的 ARGB_8888 格式的内存开销要小一半;Picasso 缓存的是全尺寸的(只缓存一种),而 Glide 缓存的是跟 ImageView 尺寸相同的(即 5656 和 128128 是两个缓存)。

    FB 的图片加载框架 Fresco:最大的优势在于 5.0 以下 (最低 2.3) 的 bitmap 加载。在 5.0 以下系统,Fresco 将图片放到一个特别的内存区域(Ashmem 区)。当然,在图片不显示的时候,占用的内存会自动被释放。这会使得 APP 更加流畅,减少因图片内存占用而引发的 OOM。为什么说是 5.0 以下,因为在 5.0 以后系统默认就是存储在 Ashmem 区了。

    3. 总结:
    Picasso 所能实现的功能,Glide 都能做,无非是所需的设置不同。但是 Picasso 体积比起 Glide 小太多如果项目中网络请求本身用的就是 okhttp 或者 retrofit(本质还是 okhttp),那么建议用 Picasso,体积会小很多(Square 全家桶的干活)。Glide 的好处是大型的图片流,比如 gif、Video,如果你们是做美拍、爱拍这种视频类应用,建议使用。

    Fresco 在 5.0 以下的内存优化非常好,代价就是体积也非常的大,按体积算 Fresco>Glide>Picasso

    不过在使用起来也有些不便(小建议:他只能用内置的一个 ImageView 来实现这些功能,用起来比较麻烦,我们通常是根据 Fresco 自己改改,直接使用他的 Bitmap 层)

    4.3、各种 json 解析库使用
    参考链接:https://www.cnblogs.com/kunpe…

    (1)Google 的 Gson
    Gson 是目前功能最全的 Json 解析神器,Gson 当初是为因应 Google 公司内部需求而由 Google 自行研发而来,但自从在 2008 年五月公开发布第一版后已被许多公司或用户应用。Gson 的应用主要为 toJson 与 fromJson 两个转换函数,无依赖,不需要例外额外的 jar,能够直接跑在 JDK 上。而在使用这种对象转换之前需先创建好对象的类型以及其成员才能成功的将 JSON 字符串成功转换成相对应的对象。类里面只要有 get 和 set 方法,Gson 完全可以将复杂类型的 json 到 bean 或 bean 到 json 的转换,是 JSON 解析的神器。Gson 在功能上面无可挑剔,但是性能上面比 FastJson 有所差距。

    (2)阿里巴巴的 FastJson
    Fastjson 是一个 Java 语言编写的高性能的 JSON 处理器, 由阿里巴巴公司开发。

    无依赖,不需要例外额外的 jar,能够直接跑在 JDK 上。FastJson 在复杂类型的 Bean 转换 Json 上会出现一些问题,可能会出现引用的类型,导致 Json 转换出错,需要制定引用。FastJson 采用独创的算法,将 parse 的速度提升到极致,超过所有 json 库。

    综上 Json 技术的比较,在项目选型的时候可以使用 Google 的 Gson 和阿里巴巴的 FastJson 两种并行使用,如果只是功能要求,没有性能要求,可以使用 google 的 Gson,如果有性能上面的要求可以使用 Gson 将 bean 转换 json 确保数据的正确,使用 FastJson 将 Json 转换 Bean

    5、热点技术
    参考链接 - Android 组件化方案

    5.1、组件化
    (1)概念:
    组件化:是将一个 APP 分成多个 module,每个 module 都是一个组件,也可以是一个基础库供组件依赖,开发中可以单独调试部分组件,组件中不需要相互依赖但是可以相互调用,最终发布的时候所有组件以 lib 的形式被主 APP 工程依赖打包成一个 apk。

    (2)由来:
    APP 版本迭代,新功能不断增加,业务变得复杂,维护成本高
    业务耦合度高,代码臃肿,团队内部多人协作开发困难
    Android 编译代码卡顿,单一工程下代码耦合严重,修改一处需要重新编译打包,耗时耗力。
    方便单元测试,单独改一个业务模块,不需要着重关注其他模块。
    (3)优势:
    组件化将通用模块独立出来,统一管理,以提高复用,将页面拆分为粒度更小的组件,组件内部出了包含 UI 实现,还可以包含数据层和逻辑层
    每个组件度可以独立编译、加快编译速度、独立打包。
    每个工程内部的修改,不会影响其他工程。
    业务库工程可以快速拆分出来,集成到其他 App 中。
    迭代频繁的业务模块采用组件方式,业务线研发可以互不干扰、提升协作效率,并控制产品质量,加强稳定性。
    并行开发,团队成员只关注自己的开发的小模块,降低耦合性,后期维护方便等。
    (4)考虑问题:
    模式切换:如何使得 APP 在单独调试跟整体调试自由切换
    组件化后的每一个业务的 module 都可以是一个单独的 APP(isModuleRun=false),release 包的时候各个业务 module 作为 lib 依赖,这里完全由一个变量控制,在根项目 gradle.properties 里面 isModuleRun=true。isModuleRun 状态不同,加载 application 和 AndroidManifest 都不一样,以此来区分是独立的 APK 还是 lib。

    在 build.grade 里面配置:
    在这里插入图片描述

    资源冲突
    当我们创建了多个 Module 的时候,如何解决相同资源文件名合并的冲突,业务 Module 和 BaseModule 资源文件名称重复会产生冲突,解决方案在于:

    每个 module 都有 app_name,为了不让资源名重名,在每个组件的 build.gradle 中增加 resourcePrefix“xxx_强行检查资源名称前缀。固定每个组件的资源前缀。但是 resourcePrefix 这个值只能限定 xml 里面的资源,并不能限定图片资源。

    依赖关系
    多个 Module 之间如何引用一些共同的 library 以及工具类

    组件通信
    组件化之后,Module 之间是相互隔离的,如何进行 UI 跳转以及方法调用,具体可以使用阿里巴巴 ARouter 或者美团的 WMRouter 等路由框架。

    各业务 Module 之前不需要任何依赖可以通过路由跳转,完美解决业务之间耦合。

    入口参数
    我们知道组件之间是有联系的,所以在单独调试的时候如何拿到其它的 Module 传递过来的参数

    Application
    当组件单独运行的时候,每个 Module 自成一个 APK,那么就意味着会有多个 Application,很显然我们不愿意重复写这么多代码,所以我们只需要定义一个 BaseApplication 即可,其它的 Application 直接继承此 BaseApplication 就 OK 了,BaseApplication 里面还可定义公用的参数。

    关于如何进行组件化,可以参考:安居客 Android 项目架构演进

    5.2、插件化
    参考链接 - 插件化入门

    (1)概述
    提到插件化,就不得不提起方法数超过 65535 的问题,我们可以通过 Dex 分包来解决,同时也可以通过使用插件化开发来解决。插件化的概念就是由宿主 APP 去加载以及运行插件 APP。

    (2 优点)
    在一个大的项目里面,为了明确的分工,往往不同的团队负责不同的插件 APP,这样分工更加明确。各个模块封装成不同的插件 APK,不同模块可以单独编译,提高了开发效率。
    解决了上述的方法数超过限制的问题。可以通过上线新的插件来解决线上的 BUG,达到“热修复”的效果。
    减小了宿主 APK 的体积。

    (3 缺点)
    插件化开发的 APP 不能在 Google Play 上线,也就是没有海外市场。

    6、屏幕适配
    6.1、基本概念
    屏幕尺寸
    含义:手机对角线的物理尺寸 单位:英寸(inch),1 英寸 =2.54cm

    Android 手机常见的尺寸有 5 寸、5.5 寸、6 寸,6.5 寸等等

    屏幕分辨率
    含义:手机在横向、纵向上的像素点数总和

    一般描述成屏幕的”宽 x 高”=AxB 含义:屏幕在横向方向(宽度)上有 A 个像素点,在纵向方向

    (高)有 B 个像素点 例子:1080×1920,即宽度方向上有 1080 个像素点,在高度方向上有 1920 个像素点

    单位:px(pixel),1px= 1 像素点

    UI 设计师的设计图会以 px 作为统一的计量单位

    Android 手机常见的分辨率:320×480、480×800、720×1280、1080×1920

    屏幕像素密度
    含义:每英寸的像素点数 单位:dpi(dots per ich)

    假设设备内每英寸有 160 个像素,那么该设备的屏幕像素密度 =160dpi

    6.2、适配方法
    1. 支持各种屏幕尺寸:使用 wrap_content, match_parent, weight. 要确保布局的灵活性并适应各种尺寸的屏幕,应使用“wrap_content”、“match_parent”控制某些视图组件的宽度和高度。

    2. 使用相对布局,禁用绝对布局。

    3. 使用 LinearLayout 的 weight 属性

    假如我们的宽度不是 0dp(wrap_content 和 0dp 的效果相同),则是 match_parent 呢?

    android:layout_weight 的真实含义是: 如果 View 设置了该属性并且有效,那么该 View 的宽度等于原有宽度 (android:layout_width) 加上剩余空间的占比。

    从这个角度我们来解释一下上面的现象。在上面的代码中,我们设置每个 Button 的宽度都是 match_parent,假设屏幕宽度为 L,那么每个 Button 的宽度也应该都为 L,剩余宽度就等于 L -(L+L)= -L。

    Button1 的 weight=1,剩余宽度占比为 1 /(1+2)= 1/3,所以最终宽度为 L +1/3*(-L)=2/3L,Button2 的计算类似,最终宽度为 L +2/3(-L)=1/3L。

    4. 使用.9 图片

    6.3、今日头条屏幕适配
    参考链接:今日头条屏幕适配方案终极版

    7、性能优化
    参考链接:Android 性能监测工具,优化内存、卡顿、耗电、APK 大小的方法
    Android 的性能优化,主要是从以下几个方面进行优化的:
    稳定(内存溢出、崩溃)
    流畅(卡顿)
    耗损(耗电、流量)
    安装包(APK 瘦身)
    影响稳定性的原因很多,比如内存使用不合理、代码异常场景考虑不周全、代码逻辑不合理等,都会对应用的稳定性造成影响。其中最常见的两个场景是:Crash 和 ANR,这两个错误将会使得程序无法使用。所以做好 Crash 全局监控,处理闪退同时把崩溃信息、异常信息收集记录起来,以便后续分析; 合理使用主线程处理业务,不要在主线程中做耗时操作,防止 ANR 程序无响应发生。

    (一)稳定——内存优化
    (1)Memory Monitor 工具:
    它是 Android Studio 自带的一个内存监视工具,它可以很好地帮助我们进行内存实时分析。通过点击 Android Studio 右下角的 Memory Monitor 标签,打开工具可以看见较浅蓝色代表 free 的内存,而深色的部分代表使用的内存从内存变换的走势图变换,可以判断关于内存的使用状态,例如当内存持续增高时,可能发生内存泄漏;当内存突然减少时,可能发生 GC 等,如下图所示。

    LeakCanary 工具:
    LeakCanary 是 Square 公司基于 MAT 开发的一款监控 Android 内存泄漏的开源框架。其工作的原理是:
    监测机制利用了 Java 的 WeakReference 和 ReferenceQueue,通过将 Activity 包装到 WeakReference 中,被 WeakReference 包装过的 Activity 对象如果被回收,该 WeakReference 引用会被放到 ReferenceQueue 中,通过监测 ReferenceQueue 里面的内容就能检查到 Activity 是否能够被回收(在 ReferenceQueue 中说明可以被回收,不存在泄漏;否则,可能存在泄漏,LeakCanary 是执行一遍 GC,若还未在 ReferenceQueue 中,就会认定为泄漏)。

    如果 Activity 被认定为泄露了,就抓取内存 dump 文件 (Debug.dumpHprofData);之后通过 HeapAnalyzerService.runAnalysis 进行分析内存文件分析;接着通过 HeapAnalyzer (checkForLeak—findLeakingReference—findLeakTrace) 来进行内存泄漏分析。最后通过 DisplayLeakService 进行内存泄漏的展示。

    (3)Android Lint 工具:
    Android Lint Tool 是 Android Sutido 种集成的一个 Android 代码提示工具,它可以给你布局、代码提供非常强大的帮助。硬编码会提示以级别警告,例如:在布局文件中写了三层冗余的 LinearLayout 布局、直接在 TextView 中写要显示的文字、字体大小使用 dp 而不是 sp 为单位,就会在编辑器右边看到提示。

    (二)流畅——卡顿优化
    卡顿的场景通常是发生在用户交互体验最直接的方面。影响卡顿的两大因素,分别是界面绘制和数据处理。

    界面绘制:主要原因是绘制的层级深、页面复杂、刷新不合理,由于这些原因导致卡顿的场景更多出现在 UI 和启动后的初始界面以及跳转到页面的绘制上。

    数据处理:导致这种卡顿场景的原因是数据处理量太大,一般分为三种情况,一是数据在处理 UI 线程,二是数据处理占用 CPU 高,导致主线程拿不到时间片,三是内存增加导致 GC 频繁,从而引起卡顿。

    (1)布局优化
    在 Android 种系统对 View 进行测量、布局和绘制时,都是通过对 View 数的遍历来进行操作的。如果一个 View 数的高度太高就会严重影响测量、布局和绘制的速度。Google 也在其 API 文档中建议 View 高度不宜哦过 10 层。现在版本种 Google 使用 RelativeLayout 替代 LineraLayout 作为默认根布局,目的就是降低 LineraLayout 嵌套产生布局树的高度,从而提高 UI 渲染的效率。

    布局复用,使用标签重用 layout;
    提高显示速度,使用延迟 View 加载;
    减少层级,使用标签替换父级布局;
    注意使用 wrap_content,会增加 measure 计算成本;
    删除控件中无用属性;

    (2)绘制优化
    过度绘制是指在屏幕上的某个像素在同一帧的时间内被绘制了多次。在多层次重叠的 UI 结构中,如果不可见的 UI 也在做绘制的操作,就会导致某些像素区域被绘制了多次,从而浪费了多余的 CPU 以及 GPU 资源。如何避免过度绘制?

    布局上的优化。移除 XML 中非必须的背景,移除 Window 默认的背景、按需显示占位背景图片

    自定义 View 优化。使用 canvas.clipRect() 帮助系统识别那些可见的区域,只有在这个区域内才会被绘制。

    (3)启动优化
    应用一般都有闪屏页 SplashActivity,优化闪屏页的 UI 布局,可以通过 Profile GPU Rendering 检测丢帧情况。

    (三)节省——耗电优化
    在 Android5.0 以前,关于应用电量消耗的测试即麻烦又不准确,而 5.0 之后 Google 专门引入了一个获取设备上电量消耗信息的 API—— Battery Historian。Battery Historian 是一款由 Google 提供的 Android 系统电量分析工具,直观地展示出手机的电量消耗过程,通过输入电量分析文件,显示消耗情况。

    最后提供一些可供参考耗电优化的方法:

    (1)计算优化。算法、for 循环优化、Switch…case 替代 if…else、避开浮点运算。
    浮点运算:计算机里整数和小数形式就是按普通格式进行存储,例如 1024、3.1415926 等等,这个没什么特点,但是这样的数精度不高,表达也不够全面,为了能够有一种数的通用表示法,就发明了浮点数。浮点数的表示形式有点像科学计数法(.×10),它的表示形式是 0.×10,在计算机中的形式为 . e ±*),其中前面的星号代表定点小数,也就是整数部分为 0 的纯小数,后面的指数部分是定点整数。利用这样的形式就能表示出任意一个整数和小数,例如 1024 就能表示成 0.1024×10^4,也就是 .1024e+004,3.1415926 就能表示成 0.31415926×10^1,也就是 .31415926e+001,这就是浮点数。浮点数进行的运算就是浮点运算。浮点运算比常规运算更复杂,因此计算机进行浮点运算速度要比进行常规运算慢得多。

    (2)避免 Wake Lock 使用不当。
    Wake Lock 是一种锁的机制,主要是相对系统的休眠而言的,, 只要有人拿着这个锁,系统就无法进入休眠意思就是我的程序给 CPU 加了这个锁那系统就不会休眠了,这样做的目的是为了全力配合我们程序的运行。有的情况如果不这么做就会出现一些问题,比如微信等及时通讯的心跳包会在熄屏不久后停止网络访问等问题。所以微信里面是有大量使用到了 Wake_Lock 锁。系统为了节省电量,CPU 在没有任务忙的时候就会自动进入休眠。有任务需要唤醒 CPU 高效执行的时候,就会给 CPU 加 Wake_Lock 锁。大家经常犯的错误,我们很容易去唤醒 CPU 来工作,但是很容易忘记释放 Wake_Lock。

    (3)使用 Job Scheduler 管理后台任务。
    在 Android 5.0 API 21 中,google 提供了一个叫做 JobScheduler API 的组件,来处理当某个时间点或者当满足某个特定的条件时执行一个任务的场景,例如当用户在夜间休息时或设备接通电源适配器连接 WiFi 启动下载更新的任务。这样可以在减少资源消耗的同时提升应用的效率。

    (四)安装包——APK 瘦身
    (1)安装包的组成结构
    assets 文件夹。存放一些配置文件、资源文件,assets 不会自动生成对应的 ID,而是通过 AssetManager 类的接口获取。

    res。res 是 resource 的缩写,这个目录存放资源文件,会自动生成对应的 ID 并映射到 .R 文件中,访问直接使用资源 ID。

    META-INF。保存应用的签名信息,签名信息可以验证 APK 文件的完整性。

    AndroidManifest.xml。这个文件用来描述 Android 应用的配置信息,一些组件的注册信息、可使用权限等。

    classes.dex。Dalvik 字节码程序,让 Dalvik 虚拟机可执行,一般情况下,Android 应用在打包时通过 Android SDK 中的 dx 工具将 Java 字节码转换为 Dalvik 字节码。

    resources.arsc。记录着资源文件和资源 ID 之间的映射关系,用来根据资源 ID 寻找资源。

    (2)减少安装包大小
    代码混淆。使用 IDE 自带的 proGuard 代码混淆器工具,它包括压缩、优化、混淆等功能。
    资源优化。比如使用 Android Lint 删除冗余资源,资源文件最少化等。
    图片优化。比如利用 PNG 优化工具 对图片做压缩处理。推荐目前最先进的压缩工具 Googlek 开源库 zopfli。如果应用在 0 版本以上,推荐使用 WebP 图片格式。
    避免重复或无用功能的第三方库。例如,百度地图接入基础地图即可、讯飞语音无需接入离线、图片库 GlidePicasso 等。
    插件化开发。比如功能模块放在服务器上,按需下载,可以减少安装包大小。
    可以使用微信开源资源文件混淆工具——AndResGuard。一般可以压缩 apk 的 1M 左右大。

    7.1、冷启动与热启动
    参考链接:https://www.jianshu.com/p/03c…

    冷启动
    在启动应用时,系统中没有该应用的进程,这时系统会创建一个新的进程分配给该应用;

    热启动
    在启动应用时,系统中已有该应用的进程(例:按 back 键、home 键,应用虽然会退出,但是该应用的进程还是保留在后台);

    区别
    冷启动:系统没有该应用的进程,需要创建一个新的进程分配给应用,所以会先创建和初始化 Application 类,再创建和初始化 MainActivity 类(包括一系列的测量、布局、绘制),最后显示在界面上。热启动:从已有的进程中来启动,不会创建和初始化 Application 类,直接创建和初始化 MainActivity 类(包括一系列的测量、布局、绘制),最后显示在界面上。

    冷启动流程
    Zygote 进程中 fork 创建出一个新的进程;创建和初始化 Application 类、创建 MainActivity;inflate 布局、当 onCreate/onStart/onResume 方法都走完;contentView 的 measure/layout/draw 显示在界面上。

    冷启动优化
    减少在 Application 和第一个 Activity 的 onCreate()方法的工作量;不要让 Application 参与业务的操作;不要在 Application 进行耗时操作;不要以静态变量的方式在 Application 中保存数据;减少布局的复杂性和深度;

    8、MVP 模式架构
    8.1、MVP 模式
    MVP 架构由 MVC 发展而来。在 MVP 中,M 代表 Model,V 代表 View,P 代表 Presenter。

    模型层(Model): 主要是获取数据功能,业务逻辑和实体模型。

    视图层(View):对应于 Activity 或 Fragment,负责视图的部分展示和业务逻辑用户交互

    控制层(Presenter): 负责完成 View 层与 Model 层间的交互,通过 P 层来获取 M 层中数据后返回给 V 层,使得 V 层与 M 层间没有耦合。

    在 MVP 中,Presenter 层完全将 View 层和 Model 层进行了分离,把主要程序逻辑放在 Presenter 层实现,Presenter 与具体的 View 层(Activity)是没有直接的关联,是通过定义接口来进行交互的,从而使得当 View 层(Activity)发生改变时,Persenter 依然可以保持不变。View 层接口类只应该只有 set/get 方法,及一些界面显示内容和用户输入,除此之外不应该有多余的内容。绝不允许 View 层直接访问 Model 层,这是与 MVC 最大区别之处,也是 MVP 核心优点。

    9、虚拟机
    9.1、Android Dalvik 虚拟机和 ART 虚拟机对比
    Dalvik
    Android4.4 及以前使用的都是 Dalvik 虚拟机,我们知道 Apk 在打包的过程中会先将 java 等源码通过 javac 编译成.class 文件,但是我们的 Dalvik 虚拟机只会执行.dex 文件,这个时候 dx 会将.class 文件转换成 Dalvik 虚拟机执行的.dex 文件。Dalvik 虚拟机在启动的时候会先将.dex 文件转换成快速运行的机器码,又因为 65535 这个问题,导致我们在应用冷启动的时候有一个合包的过程,最后导致的一个结果就是我们的 app 启动慢,这就是 Dalvik 虚拟机的 JIT 特性(Just In Time)。

    ART
    ART 虚拟机是在 Android5.0 才开始使用的 Android 虚拟机,ART 虚拟机必须要兼容 Dalvik 虚拟机的特性,但是 ART 有一个很好的特性 AOT(ahead of time),这个特性就是我们在安装 APK 的时候就将 dex 直接处理成可直接供 ART 虚拟机使用的机器码,ART 虚拟机将.dex 文件转换成可直接运行的.oat 文件,ART 虚拟机天生支持多 dex,所以也不会有一个合包的过程,所以 ART 虚拟机会很大的提升 APP 冷启动速度。

    ART 优点:

    加快 APP 冷启动速度

    提升 GC 速度

    提供功能全面的 Debug 特性

    ART 缺点:

    APP 安装速度慢,因为在 APK 安装的时候要生成可运行.oat 文件

    APK 占用空间大,因为在 APK 安装的时候要生成可运行.oat 文件

    arm 处理器

    关于 ART 更详细的介绍,可以参考 Android ART 详解

    总结
    熟悉 Android 性能分析工具、UI 卡顿、APP 启动、包瘦身和内存性能优化

    熟悉 Android APP 架构设计,模块化、组件化、插件化开发

    熟练掌握 Java、设计模式、网络、多线程技术

    Java 基本知识点
    1、Java 的类加载过程
    jvm 将.class 类文件信息加载到内存并解析成对应的 class 对象的过程,注意:jvm 并不是一开始就把所有的类加载进内存中,只是在第一次遇到某个需要运行的类才会加载,并且只加载一次

    主要分为三部分:1、加载,2、链接(1. 验证,2. 准备,3. 解析),3、初始化

    1:加载
    类加载器包括 BootClassLoader、ExtClassLoader、APPClassLoader

    2:链接
    验证:(验证 class 文件的字节流是否符合 jvm 规范)

    准备:为类变量分配内存,并且进行赋初值

    解析:将常量池里面的符号引用(变量名)替换成直接引用(内存地址)过程,在解析阶段,jvm 会把所有的类名、方法名、字段名、这些符号引用替换成具体的内存地址或者偏移量。

    3:初始化
    主要对类变量进行初始化,执行类构造器的过程,换句话说,只对 static 修试的变量或者语句进行初始化。

    范例:Person person = new Person(); 为例进行说明。

    Java 编程思想中的类的初始化过程主要有以下几点:

    找到 class 文件,将它加载到内存
    在堆内存中分配内存地址
    初始化
    将堆内存地址指给栈内存中的 p 变量
    2、String、StringBuilder、StringBuffer
    StringBuffer 里面的很多方法添加了 synchronized 关键字,是可以表征线程安全的,所以多线程情况下使用它。

    执行速度:

    StringBuilder > StringBuffer > String
    StringBuilder 牺牲了性能来换取速度的,这两个是可以直接在原对象上面进行修改,省去了创建新对象和回收老对象的过程,而 String 是字符串常量(final)修试,另外两个是字符串变量,常量对象一旦创建就不可以修改,变量是可以进行修改的,所以对于 String 字符串的操作包含下面三个步骤:

    创建一个新对象,名字和原来的一样
    在新对象上面进行修改
    原对象被垃圾回收掉
    3、JVM 内存结构
    Java 对象实例化过程中,主要使用到虚拟机栈、Java 堆和方法区。Java 文件经过编译之后首先会被加载到 jvm 方法区中,jvm 方法区中很重的一个部分是运行时常量池,用以存储 class 文件类的版本、字段、方法、接口等描述信息和编译期间的常量和静态常量。

    3.1、JVM 基本结构
    类加载器 classLoader,在 JVM 启动时或者类运行时将需要的.class 文件加载到内存中。
    执行引擎,负责执行 class 文件中包含的字节码指令。
    本地方法接口,主要是调用 C /C++ 实现的本地方法及返回结果。
    内存区域(运行时数据区),是在 JVM 运行的时候操作所分配的内存区,
    主要分为以下五个部分,如下图:
    在这里插入图片描述

    方法区:用于存储类结构信息的地方,包括常量池、静态变量、构造函数等。
    Java 堆(heap):存储 Java 实例或者对象的地方。这块是 gc 的主要区域。
    Java 栈(stack):Java 栈总是和线程关联的,每当创建一个线程时,JVM 就会为这个线程创建一个对应的 Java 栈。在这个 java 栈中又会包含多个栈帧,每运行一个方法就创建一个栈帧,用于存储局部变量表、操作栈、方法返回值等。每一个方法从调用直至执行完成的过程,就对应一个栈帧在 java 栈中入栈到出栈的过程。所以 java 栈是线程私有的。
    程序计数器:用于保存当前线程执行的内存地址,由于 JVM 是多线程执行的,所以为了保证线程切换回来后还能恢复到原先状态,就需要一个独立的计数器,记录之前中断的地方,可见程序计数器也是线程私有的。
    本地方法栈:和 Java 栈的作用差不多,只不过是为 JVM 使用到的 native 方法服务的。
    3.2、JVM 源码分析
    https://www.jianshu.com/nb/12…

    4、GC 机制
    垃圾收集器一般完成两件事

    检测出垃圾;
    回收垃圾;
    4.1 Java 对象引用
    通常,Java 对象的引用可以分为 4 类:强引用、软引用、弱引用和虚引用。
    强引用:通常可以认为是通过 new 出来的对象,即使内存不足,GC 进行垃圾收集的时候也不会主动回收。

    Object obj = new Object();
    软引用:在内存不足的时候,GC 进行垃圾收集的时候会被 GC 回收。

    Object obj = new Object();
    SoftReference<Object> softReference = new SoftReference<>(obj);
    弱引用:无论内存是否充足,GC 进行垃圾收集的时候都会回收。

    Object obj = new Object();
    WeakReference<Object> weakReference = new WeakReference<>(obj);
    虚引用:和弱引用类似,主要区别在于虚引用必须和引用队列一起使用。

    Object obj = new Object();
    ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
    PhantomReference<Object> phantomReference = new PhantomReference<>(obj, referenceQueue);
    引用队列:如果软引用和弱引用被 GC 回收,JVM 就会把这个引用加到引用队列里,如果是虚引用,在回收前就会被加到引用队列里。

    垃圾检测方法:
    引用计数法:给每个对象添加引用计数器,每个地方引用它,计数器就 +1,失效时 -1。如果两个对象互相引用时,就导致无法回收。
    可达性分析算法:以根集对象为起始点进行搜索,如果对象不可达的话就是垃圾对象。根集(Java 栈中引用的对象、方法区中常量池中引用的对象、本地方法中引用的对象等。JVM 在垃圾回收的时候,会检查堆中所有对象是否被这些根集对象引用,不能够被引用的对象就会被垃圾回收器回收。)

    垃圾回收算法:
    常见的垃圾回收算法有:

    标记 - 清除
    标记:首先标记所有需要回收的对象,在标记完成之后统计回收所有被标记的对象,它的标记过程即为上面的可达性分析算法。
    清除:清除所有被标记的对象
    缺点:
    效率不足,标记和清除效率都不高
    空间问题,标记清除之后会产生大量不连续的内存碎片,导致大对象分配无法找到足够的空间,提前进行垃圾回收。

    复制回收算法
    将可用的内存按容量划分为大小相等的 2 块,每次只用一块,当这一块的内存用完了,就将存活的对象复制到另外一块上面,然后把已使用过的内存空间一次清理掉。

    缺点:

    将内存缩小了原本的一般,代价比较高
    大部分对象是“朝生夕灭”的,所以不必按照 1:1 的比例划分。
    现在商业虚拟机采用这种算法回收新生代,但不是按 1:1 的比例,而是将内存区域划分为 eden 空间、from 空间、to 空间 3 个部分。
    其中 from 空间和 to 空间可以视为用于复制的两块大小相同、地位相等,且可进行角色互换的空间块。from 和 to 空间也称为 survivor 空间,即幸存者空间,用于存放未被回收的对象。

    在垃圾回收时,eden 空间中的存活对象会被复制到未使用的 survivor 空间中 (假设是 to),正在使用的 survivor 空间 (假设是 from) 中的年轻对象也会被复制到 to 空间中 (大对象,或者老年对象会直接进入老年带,如果 to 空间已满,则对象也会直接进入老年代)。此时,eden 空间和 from 空间中的剩余对象就是垃圾对象,可以直接清空,to 空间则存放此次回收后的存活对象。这种改进的复制算法既保证了空间的连续性,又避免了大量的内存空间浪费。

    标记 - 整理
    在老年代的对象大都是存活对象,复制算法在对象存活率教高的时候,效率就会变得比较低。根据老年代的特点,有人提出了“标记 - 压缩算法(Mark-Compact)”

    标记过程与标记 - 清除的标记一样,但后续不是对可回收对象进行清理,而是让所有的对象都向一端移动,然后直接清理掉端边界以外的内存。

    这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高。

    分带收集算法
    根据对象存活的周期不同将内存划分为几块,一般是把 Java 堆分为老年代和新生代,这样根据各个年代的特点采用适当的收集算法。

    新生代每次收集都有大量对象死去,只有少量存活,那就选用复制算法,复制的对象数较少就可完成收集。
    老年代对象存活率高,使用标记 - 压缩算法,以提高垃圾回收效率。

    5、类加载器
    程序在启动的时候,并不会一次性加载程序所要用的所有 class 文件,而是根据程序的需要,通过 Java 的类加载机制(ClassLoader)来动态加载某个 class 文件到内存当中的,从而只有 class 文件被载入到了内存之后,才能被其它 class 所引用。所以 ClassLoader 就是用来动态加载 class 文件到内存当中用的。

    5.1、双亲委派原理
    每个 ClassLoader 实例都有一个父类加载器的引用(不是继承关系,是一个包含的关系),虚拟机内置的类加载器(Bootstrap ClassLoader)本身没有父类加载器,但是可以用做其他 ClassLoader 实例的父类加载器。

    当一个 ClassLoader 实例需要加载某个类时,它会试图在亲自搜索这个类之前先把这个任务委托给它的父类加载器,这个过程是由上而下依次检查的,首先由顶层的类加载器 Bootstrap CLassLoader 进行加载,如果没有加载到,则把任务转交给 Extension CLassLoader 视图加载,如果也没有找到,则转交给 AppCLassLoader 进行加载,还是没有的话,则交给委托的发起者,由它到指定的文件系统或者网络等 URL 中进行加载类。还没有找到的话,则会抛出 CLassNotFoundException 异常。否则将这个类生成一个类的定义,并将它加载到内存中,最后返回这个类在内存中的 Class 实例对象。

    5.2、为什么使用双亲委托模型
    JVM 在判断两个 class 是否相同时,不仅要判断两个类名是否相同,还要判断是否是同一个类加载器加载的。

    避免重复加载,父类已经加载了,则子 CLassLoader 没有必要再次加载。
    考虑安全因素,假设自定义一个 String 类,除非改变 JDK 中 CLassLoader 的搜索类的默认算法,否则用户自定义的 CLassLoader 如法加载一个自己写的 String 类,因为 String 类在启动时就被引导类加载器 Bootstrap CLassLoader 加载了。

    关于 Android 的双亲委托机制,可以参考 android classloader 双亲委托模式

    6、集合
    Java 集合类主要由两个接口派生出:Collection 和 Map,这两个接口是 Java 集合的根接口。

    Collection 接口是集合类的根接口,Java 中没有提供这个接口的直接的实现类。但是却让其被继承产生了两个接口,就是 Set 和 List。Set 中不能包含重复的元素。List 是一个有序的集合,可以包含重复的元素,提供了按索引访问的方式。

    Map 是 Java.util 包中的另一个接口,它和 Collection 接口没有关系,是相互独立的,但是都属于集合类的一部分。Map 包含了 key-value 对。Map 不能包含重复的 key,但是可以包含相同的 value。

    6.1、区别
    List,Set 都是继承自 Collection 接口,Map 则不是;
    List 特点:元素有放入顺序,元素可重复; Set 特点:元素无放入顺序,元素不可重复,重复元素会覆盖掉,(注意:元素虽然无放入顺序,但是元素在 set 中的位置是有该元素的 HashCode 决定的,其位置其实是固定的,加入 Set 的 Object 必须定义 equals() 方法;
    LinkedList、ArrayList、HashSet 是非线程安全的,Vector 是线程安全的;
    HashMap 是非线程安全的,HashTable 是线程安全的;

    6.2、List 和 Vector 比较
    Vector 是多线程安全的,线程安全就是说多线程访问同一代码,不会产生不确定的结果。而 ArrayList 不是,这个可以从源码中看出,Vector 类中的方法很多有 synchronized 进行修饰,这样就导致了 Vector 在效率上无法与 ArrayList 相比;
    两个都是采用的线性连续空间存储元素,但是当空间不足的时候,两个类的增加方式是不同。
    Vector 可以设置增长因子,而 ArrayList 不可以。
    Vector 是一种老的动态数组,是线程同步的,效率很低,一般不赞成使用。

    6.3、HashSet 如何保证不重复
    HashSet 底层通过 HashMap 来实现的,在往 HashSet 中添加元素是

    public boolean add(E e) {
    return map.put(e, PRESENT)==null;
    }

    // Dummy value to associate with an Object in the backing Map
    private static final Object PRESENT = new Object();
    在 HashMap 中进行查找是否存在这个 key,value 始终是一样的,主要有以下几种情况:

    如果 hash 码值不相同,说明是一个新元素,存;
    如果 hash 码值相同,且 equles 判断相等,说明元素已经存在,不存;
    如果 hash 码值相同,且 equles 判断不相等,说明元素不存在,存;
    如果有元素和传入对象的 hash 值相等,那么,继续进行 equles()判断,如果仍然相等,那么就认为传入元素已经存在,不再添加,结束,否则仍然添加;
    6.4、HashSet 与 Treeset 的适用场景
    HashSet 是基于 Hash 算法实现的,其性能通常都优于 TreeSet。为快速查找而设计的 Set,我们通常都应该使用 HashSet,在我们需要排序的功能时,我们才使用 TreeSet。
    TreeSet 是二叉树(红黑树的树据结构)实现的,Treeset 中的数据是自动排好序的,不允许放入 null 值
    HashSet 是哈希表实现的,HashSet 中的数据是无序的,可以放入 null,但只能放入一个 null,两者中的值都不能重复,就如数据库中唯一约束。
    HashSet 是基于 Hash 算法实现的,其性能通常都优于 TreeSet。为快速查找而设计的 Set,我们通常都应该使用 HashSet,在我们需要排序的功能时,我们才使用 TreeSet。
    6.5、HashMap 与 TreeMap、HashTable 的区别及适用场景
    HashMap 非线程安全,基于哈希表(散列表) 实现。使用 HashMap 要求添加的键类明确定义了 hashCode()和 equals()[可以重写 hashCode()和 equals()],为了优化 HashMap 空间的使用,您可以调优初始容量和负载因子。其中散列表的冲突处理主要分两种,一种是开放定址法,另一种是链表法。HashMap 的实现中采用的是链表法。
    TreeMap:非线程安全基于红黑树实现,TreeMap 没有调优选项,因为该树总处于平衡状态

    7、常量池
    7.1、Interger 中的 128(-128~127)
    当数值范围为 -128~127 时:如果两个 new 出来 Integer 对象,即使值相同,通过“”比较结果为 false,但两个对象直接赋值,则通过“”比较结果为“true,这一点与 String 非常相似。
    当数值不在 -128~127 时,无论通过哪种方式,即使两个对象的值相等,通过“”比较,其结果为 false;
    当一个 Integer 对象直接与一个 int 基本数据类型通过“”比较,其结果与第一点相同;
    Integer 对象的 hash 值为数值本身;

    @Override
    public int hashCode() {
    return Integer.hashCode(value);
    }
    7.2、为什么是 -128-127?
    在 Integer 类中有一个静态内部类 IntegerCache,在 IntegerCache 类中有一个 Integer 数组,用以缓存当数值范围为 -128~127 时的 Integer 对象。

    8、泛型
    泛型是 Java SE 1.5 的新特性,泛型的本质是参数化类型,也就是说所操作的数据类型被指定为一个参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。Java 语言引入泛型的好处是安全简单。

    泛型的好处是在编译的时候检查类型安全,并且所有的强制转换都是自动和隐式的,提高代码的重用率。

    它提供了编译期的类型安全,确保你只能把正确类型的对象放入 集合中,避免了在运行时出现 ClassCastException。

    使用 Java 的泛型时应注意以下几点:

    泛型的类型参数只能是类类型(包括自定义类),不能是简单类型。
    同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的。
    泛型的类型参数可以有多个。
    泛型的参数类型可以使用 extends 语句,例如。习惯上称为“有界类型”。
    泛型的参数类型还可以是通配符类型。例如 Class<?> classType =
    Class.forName(“java.lang.String”);
    8.1 T 泛型和通配符泛型
    ?表示不确定的 java 类型。
    T 表示 java 类型。
    K V 分别代表 java 键值中的 Key Value。
    E 代表 Element。
    8.2 泛型擦除
    Java 中的泛型基本上都是在编译器这个层次来实现的。在生成的 Java 字节码中是不包含泛型中的类型信息的。使用泛型的时候加上的类型参数,会在编译器在编译的时候去掉。这个过程就称为类型擦除。

    泛型是通过类型擦除来实现的,编译器在编译时擦除了所有类型相关的信息,所以在运行时不存在任何类型相关的信息。例如 List 在运行时仅用一个 List 来表示。这样做的目的,是确保能和 Java 5 之前的版本开发二进制类库进行兼容。你无法在运行时访问到类型参数,因为编译器已经把泛型类型转换成了原始类型。

    8.3 限定通配符
    限定通配符对类型进行了限制。

    一种是 <? extends T> 它通过确保类型必须是 T 的子类来设定类型的上界,
    另一种是 <? super T> 它通过确保类型必须是 T 的父类来设定类型的下界。
    另一方面 <?> 表 示了非限定通配符,因为 <?> 可以用任意类型来替代。
    例如 List<? extends Number> 可以接受 List 或 List。

    8.4 泛型面试题
    你可以把 List 传递给一个接受 List 参数的方法吗?
    对任何一个不太熟悉泛型的人来说,这个 Java 泛型题目看起来令人疑惑,因为乍看起来 String 是一种 Object,所以 List 应当可以用在需要 List 的地方,但是事实并非如此。真这样做的话会导致编译错误。如 果你再深一步考虑,你会发现 Java 这样做是有意义的,因为 List 可以存储任何类型的对象包括 String, Integer 等等,而 List 却只能用来存储 Strings。

    Array 中可以用泛型吗?
    Array 事实上并不支持泛型,这也是为什么 Joshua Bloch 在 Effective Java 一书中建议使用 List 来代替 Array,因为 List 可以提供编译期的类型安全保证,而 Array 却不能。

    Java 中 List 和原始类型 List 之间的区别?
    原始类型和带参数类型之间的主要区别是,在编译时编译器不会对原始类型进行类型安全检查,却会对带参数的类型进行检查,通过使用 Object 作为类型,可以告知编译器该方法可以接受任何类型的对象,比如 String 或 Integer。这道题的考察点在于对泛型中原始类型的正确理解。它们之间的第二点区别是,你可以把任何带参数的类型传递给原始类型 List,但却不能把 List 传递给接受 List 的方法,因为会产生编译错误。

    List<?> 是一个未知类型的 List,而 List 其实是任意类型的 List。你可以把 List, List 赋值给 List<?>,却不能把 List 赋值给 List。

    9、反射
    9.1、概念
    JAVA 反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法;这种动态获取的信息以及动态调用对象的方法的功能称为 java 语言的反射机制。

    9.2、作用
    Java 反射机制主要提供了以下功能:在运行时判断任意一个对象所属的类;在运行时构造任意一个类的对象;在运行时判断任意一个类所具有的成员变量和方法;在运行时调用任意一个对象的方法;生成动态代理。

    10、代理
    代理这个词大家肯定已经非常熟悉,因为现实中接触的很多,其实现实中的东西恰恰可以非常形象和直观地反映出模式的抽象过程以及本质。现在房子不是吵得热火朝天吗?我们就以房子为例,来拨开代理的面纱。

    假设你有一套房子要卖,一种方法是你直接去网上发布出售信息,然后直接带要买房子的人来看房子、过户等一直到房子卖出去,但是可能你很忙,你没有时间去处理这些事情,所以你可以去找中介,让中介帮你处理这些琐碎事情,中介实际上就是你的代理。本来是你要做的事情,现在中介帮助你一一处理,对于买方来说跟你直接交易跟同中介直接交易没有任何差异,买方甚至可能觉察不到你的存在,这实际上就是代理的一个最大好处。

    接下来我们再深入考虑一下为什么你不直接买房子而需要中介?其实一个问题恰恰解答了什么时候该用代理模式的问题。

    原因一:你可能在外地上班,买房子的人没法找到你直接交易。

    对应到我们程序设计的时候就是:客户端无法直接操作实际对象。那么为什么无法直接操作?一种情况是你需要调用的对象在另外一台机器上,你需要跨越网络才能访问,如果让你直接 coding 去调用,你需要处理网络连接、处理打包、解包等等非常复杂的步骤,所以为了简化客户端的处理,我们使用代理模式,在客户端建立一个远程对象的代理,客户端就象调用本地对象一样调用该代理,再由代理去跟实际对象联系,对于客户端来说可能根本没有感觉到调用的东西在网络另外一端,这实际上就是 Web Service 的工作原理。另一种情况虽然你所要调用的对象就在本地,但是由于调用非常耗时,你怕影响你正常的操作,所以特意找个代理来处理这种耗时情况,一个最容易理解的就是 Word 里面装了很大一张图片,在 word 被打开的时候我们肯定要加载里面的内容一起打开,但是如果等加载完这个大图片再打开 Word 用户等得可能早已经跳脚了,所以我们可以为这个图片设置一个代理,让代理慢慢打开这个图片而不影响 Word 本来的打开的功能。申明一下我只是猜可能 Word 是这么做的,具体到底怎么做的,俺也不知道。

    原因二:你不知道怎么办过户手续,或者说除了你现在会干的事情外,还需要做其他的事情才能达成目的。

    对应到我们程序设计的时候就是:除了当前类能够提供的功能外,我们还需要补充一些其他功能。最容易想到的情况就是权限过滤,我有一个类做某项业务,但是由于安全原因只有某些用户才可以调用这个类,此时我们就可以做一个该类的代理类,要求所有请求必须通过该代理类,由该代理类做权限判断,如果安全则调用实际类的业务开始处理。可能有人说为什么我要多加个代理类?我只需要在原来类的方法里面加上权限过滤不就完了吗?在程序设计中有一个类的单一性原则问题,这个原则很简单,就是每个类的功能尽可能单一。为什么要单一,因为只有功能单一这个类被改动的可能性才会最小,就拿刚才的例子来说,如果你将权限判断放在当前类里面,当前这个类就既要负责自己本身业务逻辑、又要负责权限判断,那么就有两个导致该类变化的原因,现在如果权限规则一旦变化,这个类就必需得改,显然这不是一个好的设计。

    好了,原理的东西已经讲得差不多了,要是再讲个没完可能大家要扔砖头了。呵呵,接下来就看看怎么来实现代理。

    数据结构与算法
    https://zhuanlan.zhihu.com/p/…

    http://crazyandcoder.tech/201… 算法与数据结构 - 排序 /

    1、排序
    排序有内部排序和外部排序,内部排序是数据记录在内存中进行排序,而外部排序是因排序的数据很大,一次不能容纳全部的排序记录,在排序过程中需要访问外存。

    1.1、直接插入排序
    思想:

    将第一个数和第二个数排序,然后构成一个有序序列
    将第三个数插入进去,构成一个新的有序序列。
    对第四个数、第五个数……直到最后一个数,重复第二步。
    代码:

    首先设定插入次数,即循环次数,for(int i=1;i<length;i++),1 个数的那次不用插入。
    设定插入数和得到已经排好序列的最后一个数的位数。insertNum 和 j =i-1。

    2、设计模式
    参考:Android 开发中的一些设计模式

    2.1、单例设计模式
    单例主要分为:懒汉式单例、饿汉式单例、登记式单例。

    特点:

    单例类只有一个实例
    单例类必须自己创建自己的唯一实例
    单例类必须给所有其他对象提供这一实例。
    在计算机系统中,像线程池,缓存、日志对象、对话框、打印机等常被设计成单例。

    懒汉式单例:
    Singleton 通过将构造方法限定为 private 避免了类在外部被实例化,在同一个虚拟机范围内,Singleton 的唯一实例只能通过 getInstance()方法访问。(事实上,通过 Java 反射机制是能够实例化构造方法为 private 的类的,那基本上会使所有的 Java 单例实现失效。
    在这里插入图片描述
    它是线程不安全的,并发情况下很有可能出现多个 Singleton 实例,要实现线程安全,有以下三种方式:
    1. 在 getInstance 方法上加上同步
    在这里插入图片描述
    2. 双重检查锁定
    在这里插入图片描述
    3. 静态内部类
    在这里插入图片描述
    这种方式对比前两种,既实现了线程安全,又避免了同步带来的性能影响。

    饿汉式单例:
    饿汉式在创建类的同时就已经创建好了一个静态的对象供系统使用,以后不再改变,所以天生是系统安全。
    在这里插入图片描述

    其他
    https://juejin.im/post/5a3717…
    https://github.com/Mr-YangChe…

    Markdown 已选中 40397 字数 1138 行数 当前行 1138, 当前列 0 HTML 37074 字数 713 段落

    退出移动版