Android面试知识


Android Interview

Android的大体架构图

分为四个层次:linux内核;libraies和Android runntime;framework;Application

img

Android的四大组件是哪些,它们的作用?

  1. Activity:Activity是Android程序与用户交互的窗口,对用户来说是可见的
  2. service:后台服务于Activity,是一个服务,不可见
  3. Content Provider:对外提提供数据
  4. BroadCast Receiver:接受一种或者多种Intent作触发事件,接受相关消息,做一些简单处理

Android 中进程的优先级

  1. 前台进程
  2. 可见进程
  3. 服务进程
  4. 后台进程
  5. 空进程

Android中asset和res目录的区别

  1. res目录下的资源文件会在R文件中生成对应的id,asset不会\
  2. res目录下的文件在生成apk时,除raw(即res/raw)目录下文件不进行编译外,都会被编译成二进制文件;asset目录下的文件不会进行编译
  3. asset目录允许有子目录

Android中App 是如何沙箱化的,为何要这么做

  1. 沙箱化可以提升安全性和效率
  2. Android的底层内核为Linux,因此继承了Linux良好的安全性,并对其进行了优化。在Linux中,一个用户对应一个uid,而在Android中,(通常)一个APP对应一个uid,拥有独立的资源和空间,与其他APP互不干扰。如有两个APP A和B,A并不能访问B的资源,A的崩溃也不会对B造成影响,从而保证了安全性和效率

Activity


Activity 生命周期

img

Activity在屏幕旋转时的生命周期

  1. 没有任何设置时,会调用整个生命周期方法,并且会调用onSaveInstance和onRestoreInstanceState方法
  2. 在Manifest中为Activity设置android:configChanges=”orientation”时,只调用onConfigChanges方法
  3. android:configChanges=”orientation”属性有可能不起作用,依然会调用整个生命周期方法,这是因为不同版本处理方式可能不同,有时候还需要加上android:configChanges=”orientation|keyboardHidden|screenSize”等。

onSaveInstanceState 什么时候调用

  1. 非用户主动明确结束(按back键,自定义click方法调用finish)时都会调用onSaveInstanceState:
    1. 屏幕旋转
    2. 按HOME键
    3. 内存不足
    4. 从一个activity启动另一个activity
  2. 这个方法的调用时机是在onStop前,但是它和onPause没有既定的时序关系

自定义View控件的状态被保存需要满足两个条件

  1. View有唯一的ID
  2. View的初始化时要调用setSaveEnabled(true)

configChanges属性

对Activity配置了android:configChanges=”xxx”属性之后,Activity就不会在对应变化发生时重新创建,而是调用Activity的onConfigurationChanged方法。常用的有local:设备的本地位置发生了变化,一般指切换了系统语言;keyboardHidden:键盘的可访问性发生了变化,比如用户调出了键盘;orientation:屏幕方向发生了变化,比如旋转了手机屏幕。

A activity启动B activity和B activity返回A activity的生命周期执行过程

  1. A启动B:A.onPause()→B.onCreate()→B.onStart()→B.onResume()→A.onStop
  2. B返回A:B.onPause()→A.onRestart()/A.onCreate()→A.onStart()→A.onResume()→B.onStop()

Activity执行finish后的生命周期

  1. 在onCreate中执行:onCreate -> onDestroy
  2. 在onStart中执行:onCreate -> onStart -> onStop -> onDestroy
  3. 在onResume中执行:onCreate -> onStart -> onResume -> onpause -> onStop -> onDestroy

如果用了一些解耦的策略,怎么管理生命周期的?

  1. 可以用Google的

    LifeCycle框架

    \0. 引入LifeCycle框架

    1. 将控件实现LifecycleObserver接口
    2. 在Activity中中注册控件:getLifeCycle().addObderver(View);
    3. 在控件中使用: @OnLifecycleEvent(Lifecycle.Event.ON_START)

Activity的启动流程

img

Android中Activity的启动模式

  1. standard:每一次启动,都会生成一个新的实例,放入栈顶中
  2. singleTop:通过singelTop启动Activity时,如果发现有需要启动的实例正在栈顶,责直接重用,否则生成新的实例
  3. singleTask:通过singleTask启动Activity时,如果发现有需要启动的实例正在栈中,责直接移除它上边的实例,并重用该实例,否则生成新的实例
  4. singleInstance:通过singleTask启动Activity时,会启用一个新的栈结构,并将新生成的实例放入栈中。

TaskAffinity 属性

  1. 任务相关性,标识一个Activity所需的任务栈的名字。默认情况下,所有的Activity所需的任务栈的名字是应用的包名,当然也可以单独指定TaskAffinity属性。
  2. TaskAffinity属性主要和singleTask启动模式和allowTaskRepeating属性配对使用,在其他情况下使用没有意义
  3. 当TaskAffinity和singleTask启动模式配对使用的时候,它是具有该模式的Activity的目前任务栈的名字,待启动的Activity会运行在名字和TaskAffinity相同的任务栈中
  4. 当TaskAffinity和allowTaskReparenting结合的时候,当一个应用A启动了应用B的某个Activity C后,如果Activity C的allowTaskReparenting属性设置为true的话,那么当应用B被启动后,系统会发现Activity C所需的任务栈存在了,就将Activity C从A的任务栈中转移到B的任务栈中。

Activity启动模式的TaskAffinity和allowTaskReparenting

  1. TaskAffinity配合singleTask使用,指定任务栈:如果没有TaskAffinity指定的任务栈,则开启新栈
  2. allowTaskReparenting配合standard和singleTop使用,标明该Activity的任务栈可以重新设置

当前应用有两个Activity A和B,B的 android:launchMode 设置了singleTask模式,A是默认的standard,那么A startActivity启动B,B会新启一个Task吗?如果不会,那么startActivity的Intent加上FLAG_ACTIVITY_NEW_TASK这个参数会不会呢?

设置了singleTask启动模式的Activity,它在启动的时会先在系统中查看属性值affinity等于它的属性值taskAffinity ( taskAffinity默认为包名 ) 的任务栈是否存在。如果存在这样的任务栈,它就会在这个任务栈中启动,否则就会在新任务栈中启动。

当Intent对象包含FLAG_ACTIVITY_NEW_TASK标记时,系统在查找时仍然按Activity的taskAffinity属性进行匹配,如果找到一个任务栈的taskAffinity与之相同,就将目标Activity压入此任务栈中,如果找不到则创建一个新的任务栈。

设置了singleTask启动模式的Activity在已有的任务栈中已经存在相应的Activity实例,再启动它时会把这个Activity实例上面的Activity全部结束掉。也就是说singleTask自带clear top的效果。

IntentFilter的匹配规则

IntentFilter中的过滤信息有action、category、data,为了匹配过滤列表,需要同时匹配过滤列表中的action、category、data信息,否则匹配失败。

验证是否有当前Activity

  1. PackageManager的resolveActivity方法或者Intent的resolveActivity方法:如果找不到就会返回null
  2. PackageManager的queryIntentActivities方法:它返回所有成功匹配的Activity信息

如何获取当前屏幕Activity的对象?

通过在Application中注册Activity生命周期的监听函数Application.registerActivityLifecycleCallbacks()

onNewIntent调用时机

一个Activity已经启动,当再次启动它时,如果他的启动模式(如SingleTask,SingleTop)标明不需要重新启动,会调用onNewIntent

除了用Intent 去启动一个Activity,还有其他方法吗

使用adb shell am 命令 :如adb shell am start com.example.fuchenxuan/.MainActivity 或者 adb shell am broadcast -a magcomm.action.TOUCH_LETTER

Android中子线程更新UI的方式

  1. activity.runOnUiThread(runnable)
  2. 通过主线程中的Handler进行更新
  3. 通过View的post()或者postDelayed方法进行更新

Activity之间的通信方式

  1. Intent
  2. BroadCast或者LocalBroadCast
  3. 数据存储的方式
  4. 静态变量

Service


Service的启动方式

  1. start
  2. bind

Service生命周期

img

Service 和Activity 的通信方式

  1. 如上Activity和Activity的通信方式
  2. bind方式启动时可以通过ServiceConnection通信:在SerVice的onBind方法中返回一个binder,该binder可以是AIDL方法产生的,也可以是Messenger方法产生的

Service和Thread的区别

  1. 这是没用任何关系的两个概念,servie是系统的组件,Thread是CPU运行的最小单元
  2. Service不可见,我们可以把Service当成是不可见的Activity,用于在后台执行一些服务;
  3. Service可以运行在任意线程上,如果我们生成它时没有做特殊说明,那么它运行在主线程上
  4. 很多时候我们需要在Activity中开启一个Service,再在Service中开启一个线程,这么做的原因是Service只会初始化一次,我们可以随时找到Service中生成的thread,使用场景举例:
    1. 如我们需要在多个Activity中对同一个thread进行控制时;
    2. 如果你的 Thread 需要不停地隔一段时间就要连接服务器做某种同步的话,该 Thread 需要在 Activity 没有start的时候也在运行。这个时候当你 start 一个 Activity 就没有办法在该 Activity 里面控制之前创建的 Thread。因此你便需要创建并启动一个 Service ,在 Service 里面创建、运行并控制该 Thread,这样便解决了该问题

为什么有时需要在Service中创建子线程而不是Activity中

这是因为Activity很难对Thread进行控制,当Activity被销毁之后,就没有任何其它的办法可以再重新获取到之前创建的子线程的实例。而且在一个Activity中创建的子线程,另一个Activity无法对其进行操作。但是Service就不同了,所有的Activity都可以与Service进行关联,然后可以很方便地操作其中的方法,即使Activity被销毁了,之后只要重新与Service建立关联,就又能够获取到原有的Service中Binder的实例。因此,使用Service来处理后台任务,Activity就可以放心地finish,完全不需要担心无法对后台任务进行控制的情况。

IntentService

  1. IntentService 是继承自 Service,内部通过HandlerThread启动一个新线程处理耗时操作么,可以看做是Service和HandlerThread的结合体,在完成了使命之后会自动停止,适合需要在工作线程处理UI无关任务的场景
  2. 如果启动 IntentService 多次,那么每一个耗时操作会以工作队列的方式在 IntentService 的 onHandleIntent 回调方法中执行,依次去执行,使用串行的方式,执行完自动结束。

IntentService生命周期是怎样的

  1. 在所有任务执行完毕后,自动结束生命

BroadCast


BroadCast的注册方式与区别

  1. 在manifest中静态注册:广播是常驻的,App关闭后仍能接收广播,唤醒App
  2. 动态的注册和注销:动态注册的广播生命周期和他的宿主相同,或者调用注销方法注销广播

Android中发送BroadCast的方式

  1. 无序广播:通过mContext.sendBroadcast(Intent)或mContext.sendBroadcast(Intent, String)发送的是无序广播(后者加了权限);
  2. 通过mContext.sendOrderedBroadcast(Intent, String, BroadCastReceiver, Handler, int, String, Bundle)发送的是有序广播(不再推荐使用)。
  3. 在无序广播中,所有的Receiver会接收到相同广播;而在有序广播中,我们可以为Receiver设置优先级,优先级高的先接收广播,并有权对广播进行处理和决定要不要继续向下传送

BroadCastReceiver处理耗时操作

  1. BroadcastReceiver的生命周期只有一个回调方法onReceive(Context context, Intent intent);无法进行耗时操作,即使启动线程处理,也是出于非活动状态,有可能被系统杀掉。
  2. 如果需要进行耗时操作,可以启动一个service处理。

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

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

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

  1. 广播是通过Intent携带需要传递的数据的
  2. Intent是通过Binder机制实现的
  3. Binder对数据大小有限制,不同room不一样,一般为1M

Localbroadcast

本地广播,只有本进程中的receivers能接收到此广播

实现原理(监听者模式):

  1. LocalBroadcastManager是一个单例
  2. 在LocalBroadcastManager实例中维护一个Action和ReceiverRecord的Map.(ReceiverRecord是reveiver和intentfilter的组合)
  3. 当调用LocalBroadcastManager的sendBroadcast方法时,会从2中的map找到合适的receiver,让后加到待执行的队列mPendingBroadcasts,并通过Handler发送一个空消息(此Handler运行在主线程中,是创建manager时创建的)
  4. handler 的handle方法收到消息,从mPendingBroadcasts取出receiver并调用onreceive方法
    其他:删除方法是通过一个辅助的hashmap实现的,hashmap存储了receiver和receiverRecord

ContentProvider


请介绍下ContentProvider是如何实现数据共享的

  1. 准确的说,ContentProvider是一个APP间共享数据的接口。一个程序可以通过实现一个Content provider的抽象接口将自己的数据完全暴露出去,数据可以是SqLite中的,也可以是文件或者其他类型。
  2. 使用方式:
    1. 在A APP中实现建ContentProvider,并在Manifest中生命它的Uri和权限
    2. 在B APP中注册权限,并通过ContentResolver和Uri进行增删改查
  3. 扩展:ContentProvider底层是通过Binder机制来实现跨进程间通信,通过匿名共享内存方式进行数据的传输 一个应用进程有16个Binder线程去和远程服务进行交互,而每个线程可占用的缓存空间是128KB,超过会报异常。

每个ContentProvider的操作是在哪个线程中运行的呢(其实我们关心的是UI线程和工作线程)?比如我们在UI线程调用getContentResolver().query查询数据,而当数据量很大时(或者需要进行较长时间的计算)会不会阻塞UI线程呢?

  1. ContentProvider和调用者在同一个进程,ContentProvider的方法(query/insert/update/delete等)和调用者在同一线程中
  2. ContentProvider和调用者在不同的进程,ContentProvider的方法会运行在它自身所在进程的一个Binder线程中

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

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

Fragment


Fragment生命周期

onAttach -> onCreate -> onCreateView -> onActivityCreate -> onStart -> onResume -> onPause -> onStop -> onDestoryView -> onDestory -> onDetach

img

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

举例:getActivity()空指针:这种情况一般发生在在异步任务里调用getActivity(),而Fragment已经onDetach()。

Fragment 有什么优点, Fragment和View可以相互替换嘛

  1. Fragment为了解决Andriod碎片化而产生的
  2. Fragment和View都有助于界面复用
  3. Fragment的复用粒度更大,包含生命周期和业务逻辑,通常包含好几个View
  4. View通常更关注视图的实现

Fragment add replace 区别

  1. replace 先删除容器中的内容,再添加
  2. add直接添加,可以配合hide适用

存储


0. Android中数据存储的方式有哪些

  1. File
  2. SharedPreferences
  3. SQlite
  4. 网络
  5. ContentProvider

1. SharedPreference是进程同步的嘛,有没有什么方法进程同步

  1. 默认不是
  2. 可以设置模式MODE_MULTI_PROCESS做到进程同步,但因为该模式有很多坑,已经被Google弃用
  3. 官方建议使用ContentProvider

2. SharedPreferences commit和apply的区别

  1. commit是同步的提交,这种方式很常用,在比较早的SDK版本中就有了。这种提交方式会阻塞调用它的线程,并且这个方法会返回boolean值告知保存是否成功(如果不成功,可以做一些补救措施)。
  2. apply是异步的提交方式,目前Android Studio也会提示大家使用这种方式

3. 文件存储路径与权限权限

  1. 文件存储分为内部存储和外部存储
  2. 内部存储
    1. Environment.getDataDirectory() = /data //这个方法是获取内部存储的根路径
    2. getFilesDir().getAbsolutePath() = /data/user/0/packname/files //这个方法是获取某个应用在内部存储中的files路径
    3. getCacheDir().getAbsolutePath() = /data/user/0/packname/cache //这个方法是获取某个应用在内部存储中的cache路径
    4. getDir(“myFile”, MODE_PRIVATE).getAbsolutePath() = /data/user/0/packname/app_myFile
  3. 外部存储
    1. Environment.getExternalStorageDirectory().getAbsolutePath() = /storage/emulated/0 //这个方法是获取外部存储的根路径
    2. Environment.getExternalStoragePublicDirectory(“”).getAbsolutePath() = /storage/emulated/0 这个方法是获取外部存储的根路径 3. getExternalFilesDir(“”).getAbsolutePath() = /storage/emulated/0/Android/data/packname/files 这个方法是获取某个应用在外部存储中的files路径 4. getExternalCacheDir().getAbsolutePath() = /storage/emulated/0/Android/data/packname/cache 这个方法是获取某个应用在外部存储中的cache路径
  4. 清楚数据和卸载APP时, 内外存储的file和cache都会被删除
  5. 内部存储file和cache不需要权限;外部存储低版本上(19以下)file和cache需要权限,高版本不需要权限;Environment.getExternalStorageDirectory()需要权限

4. SQLite

  1. SQLite每个数据库都是以单个文件(.db)的形式存在,这些数据都是以B-Tree的数据结构形式存储在磁盘上。
  2. 使用SQLiteDatabase的insert,delete等方法或者execSQL方法默认都开启了事务,如果操作顺利完成才会更新.db数据库。事务的实现是依赖于名为rollback journal文件,借助这个临时文件来完成原子操作和回滚功能。在/data/data//databases/目录下看到一个和数据库同名的.db-journal文件。

如何对SQLite数据库中进行大量的数据插入?
显示的开启事务

db.beginTransaction();
try {
   ...
   db.setTransactionSuccessful();
} finally {
   db.endTransaction();
  }
复制代码

5. 数据库的升级

  1. Android中提供了SqLiteOpenHelper类,当版本更新时,会自动调用onUpgrade方法,我们在此方法中升级
  2. 如果修改已有表,可以才有临时表的方法:
    1. 将已有表重命名成一个临时表
    2. 创建新表
    3. 拷贝
    4. 删除临时表

6. 如何导入外部数据库

  1. 把数据库db文件放在res/raw下打包进apk
  2. 通过FileInputStream读取db文件,通过FileOutputStream将文件写入/data/data/包名/database下

View


0. android:gravity与android:layout_gravity的区别

  1. gravity是控制当前View内布局的位置
  2. layout_gravity是控制View在父布局中的位置

1. View的绘制流程

从ViewRootImpl的performTraversals开始,经过measure,layout,draw 三个流程。draw流程结束以后就可以在屏幕上看到view了。

2. View的measureSpec 由谁决定?顶级view呢

  1. View的MeasureSpec由父容器的MeasureSpec和其自身的LayoutParams共同确定,
  2. 而对于DecorView是由它的MeasureSpec由窗口尺寸和其自身的LayoutParams共同确定。

3. View和ViewGroup的基本绘制流程

View

  1. measure -> onMeasure
  2. layout(onLayout方法是空的,因为他没有child了)
  3. draw -> ondraw

ViewGroup

  1. measure -> onMeasure (onMeasure中需要调用childView的measure计算大小)
  2. layout -> onLayout (onLayout方法中调用childView的layout方法)
  3. draw -> onDraw (ViewGroup一般不绘制自己,ViewGroup默认实现dispatchDraw去绘制孩子)

4. draw方法 大概有几个步骤

  1. drawbackground
  2. 如果要视图显示渐变框,这里会做一些准备工作
  3. draw自身内容
  4. drawChild
  5. 如果需要, 绘制当前视图在滑动时的边框渐变效果
  6. 绘制装饰,如滚动条

5. 怎么控制另外一个进程的View显示

RemoteView:RemoteViews实现了Parcelable接口,通过binder机制传递给远程进程,进程间view的显示

6. 两指缩放

  1. 为了解决多点触控问题,android在MotionEvent中引入了pointer概念
  2. 通过ACTION_DOWN、ACTION_POINTER_DOWN、ACTION_MOVE、ACTION_POINTER_UP、ACTION_UP来检测手机的动作
  3. 每个手指的位置可以通过getX(pointIndex)来获得,这样我们就能判断出滑动的距离
  4. 缩放有多种实现: 1. ImageView可以通过setImageMatrix(martix)来实现 2. 自定义View可以缩放Canvas的大小 3. 还可以设置LayoutParams来改变大小

7. Scroller

  1. Scroller 通常用来实现平滑的滚动
  2. 实现平滑滚动:
    1. 新建Scroller,并设置合适的插值器
    2. 在View的computeScroll方法中调用scroller,查看当前应该滑动到的位置,并调用view的scrollTo或者scrollBy方法滑动
    3. 调用Scroller的start方法开始滑动

8. ScrollView时候滑动到底部

  1. 滑动时会调用onScrollChange方法,在该方法中监听状态
  2. 判断childView.getMeasureHeight(总高度) == getScrollY(滑动的高度) + chilView.getHeight(可见高度)

9. View事件的分发

  1. 思想:委托子View处理,子View不能处理则自己处理
  2. 委托过程:activity -> window -> viewGroup -> view
  3. 处理事件方法的优先级:onTouchListener > onTouchEvent > onClickListener
伪代码
public boolean dispatchTouchEvent(MotionEvent ev){
boolean consume = false;
if(onInterceptTouchEvent(ev)){
  consume = onTouchEvent(ev)
} else {
  consume = child.dispatchTouchEvent(ev);
}
return consume;
}

复制代码
  1. 完整的事件通常包括Down、Move、Up,当down事件被拦截下来以后,move和up就不再走intercept方法,而是直接被传递给当前view处理

10. 什么时候执行ACTION_CANCEL

  1. 一个点击或者活动事件包含ACTION_DOWN,ACTION_MOVE,ACTION_UP等
  2. 当子View处理了ACTION_DOWN事件之后,后续的ACTION_MOVE,ACTION_UP都会直接交由这个子View处理
  3. 如果此时父View拦截了ACTION_MOVE,ACTION_UP,那么子View会收到一个ACTION_CANCEL
  4. 场景举例:点击一个控件,并滑动到控件外,此时次控件会收到一个ACTION_CALNCEL

11. 滑动冲突

外部拦截:重写onInterceptTouchEvent方法
内部拦截:重写dispatchTouchEvent方法,同时配合requestDisAllowInterceptTouchEvent方法

12. ListView怎么优化(举例)

  1. 复用convertView,对convetView进行判空,当convertView不为空时重复使用,为空则初始化,从而减少了很多不必要的View的创建、减少findViewById的次数,
  2. 采用ViewHolder模式缓存item条目的引用
  3. 避免在getview方法中做耗时操作
  4. 避免使用半透明或者在活动中取消半透明
  5. 图片异步加载,待滑动停止后再加载
  6. 局部刷新

13. RecyclerView

RecyclerView 与 ListView 类似,都是通过缓存view提高性能,但是RecyclerView有更高的可定制性。下面是使用时的一些设置,通过这些设置来达到对view样式的自定义:其中1、2是必须设置的,3、4可选

  1. 想要控制其item们的排列方式,请使用布局管理器LayoutManager
  2. 如果要创建一个适配器,请使用RecyclerView.Adapter (Adapter通过范型的方式,帮助我们生成ViewHolder)
  3. 想要控制Item间的间隔,请使用RecyclerView.ItemDecoration
  4. 想要控制Item增删的动画,请使用RecyclerView.ItemAnimator
    扩展:RecyclerView可以很方便的进行局部刷新(notifyItemChanged())

14. RecyclerView绘制流程

RecyclerView的Measure和Layout是委托LayoutManager进行的

15. RecyclerView的局部刷新

  1. 调用notifyItemChange方法
  2. 如果想刷新某个item的局部,可以有两种方法
    1. notifyItemRangeChanged方法
    2. 根据position获取对应的ViewHolder,更新其View

16. RecyclerView的缓存

  1. RecyclerView采用四级缓存,ListView采用两级缓存
  2. 四级缓存:
    1. mAttachedScrap:用于屏幕内的itemView快速复用,不需要create和bind
    2. mCacheViews:用于屏幕外的itemView快速复用,默认为两个,通过postion查找,不需要create和bind
    3. mViewCacheExtentsion:需要用户定制,默认不实现
    4. mRecyclerPool:默认上限5个;不需要create,需要bind;多个RecyclerView可以共用一个pool
  3. 总结:缓存方面和ListView最大区别是mCacheViews可以缓存屏幕外的item,并且不需要重新bind

17. RecyclerView 自定义LayoutManager

  1. 重写onLayoutChildren方法
    1. 调用detachAndScrapAttachedViews方法,缓存View
    2. 计算并设置每个children的位置
    3. 调用fill方法
  2. 重写fill方法进行布局
  3. 重写canScrollVertically和scrollVerticallyBy方法,支持滑动

18. SurfaceView与View的区别

  1. View主要适用于主动更新的情况下,而SurfaceView主要适用于被动更新,例如频繁地刷新;
  2. View在主线程中对画面进行刷新,而SurfaceView通常会通过一个子线程来进行页面刷新
  3. 而SurfaceView在底层实现机制中就已经实现了双缓冲机制

19. view 的布局

布局全都继承自ViewGroup

  1. FrameLayout(框架布局) :没有对child view的摆布进行控制,这个布局中所有的控件都会默认出现在视图的左上角。
  2. LinearLayout(线性布局):横向或竖向排列内部View
  3. RelativeLayout(相对布局):以view的相对位置进行布局
  4. TableLayout(表格布局):将子元素的位置分配到行或列中,一个TableLayout由许多的TableRow组成
  5. GridLayout:和TableLayout类似
  6. ConstraintLayout(约束布局):和RelativeLayout类似,还可以通过GuideLine辅助布局,适合图形化操作推荐使用
  7. AbsoluteLayout(绝对布局):已经被废弃

20. 线性布局 相对布局 效率哪个高

相同层次下,因为相对布局会调用两次measure,所以线性高 当层次较多时,建议使用相对布局

21. View 的invalidate\postInvalidate\requestLayout方法

  1. invalidate 会调用onDraw进行重绘,只能在主线程
  2. postInvalidate 可以在其他线程
  3. requestLayout会调用onLayout和onMeasure,不一定会调用onDraw

22. 更新UI方式

  1. Activity.runOnUiThread(Runnable)
  2. View.post(Runnable),View.postDelay(Runnable,long)
  3. Handler

23. postDelayed原理

  1. 不管是view的postDelayed方法,还是Handler的post方法,通过包装后最终都会走Handler的sendMessageAtTime方法
  2. 随后会通过MessageQueue的enqueueMessage方法将message加入队列,加入时按时间排序,我们可以理解成Message是一个有序队列,时间是其排序依据
  3. 当Looper从MessageQueue中调用next方法取出message时,如果还没有到时间,就会阻塞等待
  4. 2中当有新的message加完成后,会检查当前有没有3中设置的阻塞,需不需要唤起,如果需要唤起则唤起

24. 当一个TextView的实例调用setText()方法后执行了什么

  1. setText后会对text做一些处理,如设置AutoLink,Ellipsize等
  2. 在合适的位置调用TextChangeListener
  3. 调用requestLayout和invalidate方法

25. 自定义View执行invalidate()方法,为什么有时候不会回调onDraw()

  1. View 的draw方法会根据很多标识位来决定是否需要调用onDraw,包括是否绑定在当前窗口等

26. View 的生命周期

  1. 构造方法
  2. onFinishInflate:该方法当View及其子View从XML文件中加载完成后会被调用。
  3. onVisibilityChanged
  4. onAttachedToWindow
  5. onMeasure
  6. onSizeChanged
  7. onLayout
  8. onDraw
  9. onWindowFocusChanged
  10. onWindowVisibilityChanged
  11. onDetachedFromWindow

27. ListView针对多种item的缓存是如何实现的

  1. 维护一个缓存view的数组,数组长度是 adapter的getViewItemTypeCount
  2. 通过getItemViewType获得缓存view 的数组,取出缓存的view

28. Canvas.save()跟Canvas.restore()的调用时机

save:用来保存Canvas的状态。save之后,可以调用Canvas的平移、放缩、旋转、错切、裁剪等操作。 restore:用来恢复Canvas之前保存的状态。防止save后对Canvas执行的操作对后续的绘制有影响。

WebView


0. WebView 优化

  1. 替换内核:不同的rom厂商对webview的优化不同,可能出现兼容性和速度问题,可以替换成X5内核等来解决兼容性问题,同时提高加载速度
  2. WebView初始化提前:WebView初始化是一个耗时的过程,我们可以预先将WebView初始化好,例如使用全局webview
  3. 开启缓存,可以明显提升第二次加载的速度
  4. 优化dns解析速度:使用和api一样的域名,这样dns不用二次解析,可以提高速度
  5. 预加载:将需要的文件资源通过native方法提前加载好或者打包进apk,需要使用时直接使用
  6. 延迟加载:延迟加载部分资源,在界面要显示的数据加载完成后再加载,如图片资源,js等
  7. 对于webview内存泄漏:单独开一个进程,Activity销毁时手动回收资源

1. WebView与Native交互

  1. WebView中拦截网址:设置setWebViewClient,重写shouldOverrideUrlLoading

  2. js与native交互

    1. mWebView.getSettings().setJavaScriptEnabled(true);
    2. mWebView.addJavascriptInterface(new JSInterface(), “jsInterface”);
    3. 其他js调用Native方案:通过prompt, alert等,在webClient中重写拦截相应的方法

性能相关


0. Android中ViewHolder,Handler等为什么要被声明成静态内部类(static)

非静态内部类会持有外部类的引用,在一些情况下很可能造成内存泄漏,所以一般声明为静态内部类,但并不是说一定要生命成静态内部类。

1. Android中捕获 App崩溃和重启

  1. 实现Thread.UncaughtExceptionHandler()接口,在uncaughtException方法中完成对崩溃的上报和对App的重启。
  2. 实现自定义Application,并在Application中注册1中Handler实例。

2. LruCache实现原理

最近最少使用算法:内部通过LinkedHashMap来实现

3. Parcelable和Serializable

  1. 都是序列化的方式
  2. Serializable只需实现Serializable接口即可
  3. Parcelable需要实现Parcelable接口,并重写writeToParcel和describeContents方法,并且实现一个Creator
  4. Serializable虽然操作简单,但是需要大量IO操作,效率慢;Parcelable自已实现封送和解封(marshalled &unmarshalled)操作不需要用反射,数据也存放在Native内存中,效率要快很多,在Android中更推荐使用Parcelable
  5. 由于不同版本Parcelable可能存在不同,在网络和磁盘存储时,推荐使用Parcelable

4. 加载合适比例的Bitmap到内存中

public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId,
        int reqWidth, int reqHeight) {

    // 首先只解析图片资源的边界
    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(res, resId, options);

    //计算缩放值
    options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);

    // 用计算出来的缩放值解析图片
    options.inJustDecodeBounds = false;
    return BitmapFactory.decodeResource(res, resId, options);
}


public static int calculateInSampleSize(
            BitmapFactory.Options options, int reqWidth, int reqHeight) {
    // Raw height and width of image
    final int height = options.outHeight;
    final int width = options.outWidth;
    int inSampleSize = 1;

    if (height > reqHeight || width > reqWidth) {

        final int halfHeight = height / 2;
        final int halfWidth = width / 2;

        // Calculate the largest inSampleSize value that is a power of 2 and keeps both
        // height and width larger than the requested height and width.
        while ((halfHeight / inSampleSize) > reqHeight
                && (halfWidth / inSampleSize) > reqWidth) {
            inSampleSize *= 2;
        }
    }

    return inSampleSize;
}

5. 图片的三级缓存

  1. 首次加载 Android App 时,肯定要通过网络交互来获取图片,之后我们可以将图片保存至本地SD卡和内存中
  2. 之后运行 App 时,优先访问内存中的图片缓存
  3. 若内存中没有,则加载本地SD卡中的图片

6. App保活

随着Google对Android系统的更新,以及国内厂商堆Room的定制,一些保活的手段已经失效或者不再适用,这里列举一些保活手段,实际中常常是多个方式并用

  1. 联系Room厂商加入白名单(或者引导用户手动加入白名单)
  2. 利用系统漏洞root进行提权,或者直接把本应用变成系统应用
  3. Service设置成START_STICKY:kill 后会被重启(等待5秒左右),重传Intent,保持与重启前一样
  4. 提升service优先级:通过android:priority = “1000”这个属性设置最高优先级(可能已经失效)
  5. onDestroy方法里重启service(效果不好)
  6. 监听广播(除了监听系统广播外,还可以利用友盟等第三方sdk做到应用相互唤起)
  7. 启动两个进程service相互监听,互相唤起(大部分room上失效)
  8. 在屏幕上保留1像素
  9. 注册系统同步服务
  10. 创建Native进程,双进程互相监听保活(5.0 以上失效)
  11. 利用JobSheduler保活

7. 如何判断APP被强杀

这里所说的强杀场景如这篇文章

  1. 在Application中定义一个static常量,赋值为-1
  2. 在欢迎界面改为0
  3. 在BaseActivity判断该常量的值

8. 常见的内存泄漏(举例)

  1. 不恰当的使用static变量(或者向static集合中添加数据却忘了必要时移除数据)
  2. 忘记关闭各种连接,如IO流等
  3. 不恰当的内部类:因为内部类持有外部类的引用,当内部类存活时间较长时,导致外部类也不能正确的回收(常发生在使用Handler的时候)
  4. 不恰当的单例模式:例如错误的将某个Activity给单例持有,或者在不该使用单例的地方使用了单例
  5. 使用错误的Context:Application 和 Activity的context生命周期不一样
  6. webview造成的内存泄漏

9. OOM异常是否可以被try…catch捕获

  1. 在发生地点可以捕获
  2. 但是OOM往往是由于内存泄漏造成的,泄漏的部分多数情况下不在try语句块里,所以catch后不久就会再次发生OOM
  3. 对待OOM的方案应该是找到内存泄漏的地方以及优化内存的占用

10. 什么是ANR,如何避免它

  1. ANR是Application Not Responding 的缩写,当应用程序无响应时,会弹出ANR窗口,让用户选择继续等待还是关闭应用。
  2. 处理超过时间会造成ANR的地方:触摸操作等(5s);BroadCast(10s);Service(20s)
  3. 避免:不要在主线程中做耗时操作

11. 什么情况会导致Force Close ?如何避免?能否捕获导致其的异常?

  1. 出现运行时异常(如nullpointer/数组越界等),而我们又没有try catch捕获,可能造成Force Close
  2. 避免:需要我们在编程时谨慎处理逻辑,提高代码健壮性。如对网络传过来的未知数据先判空,再处理;此外还可以通过静态代码检查来帮助我们提高代码质量
  3. 此外,我们还可以在Application初始化时注册UncaultExceptionHandler,来捕捉这些异常重启我们的程序

12. DDMS和TraceView

  1. DDMS是一个程序执行查看器,在里面可以看见线程和堆栈等信息
  2. TraceView是一个性能分析器
    扩展: Android Studio 3 中用Profiler代替了DDMS,可以监视分析CPU,网络,内存的实时情况,更加方便

13. LRUCache原理

  1. 通过LinkedHashMap实现的
  2. LinkedHashMap的特性:LinkedHashMap是一个双向链表,当构造函数中accessOrder为true时:调用put()方法,会在集合头部添加元素时,会调用trimToSize()判断缓存是否已满,如果满了就用LinkedHashMap的迭代器删除队尾元素,即近期最少访问的元素。当调用get()方法访问缓存对象时,就会调用LinkedHashMap的get()方法获得对应集合元素,同时会更新该元素到队头

14. 模块化的好处

  1. 不同模块间解偶,方便复用
  2. 单个模块支持独立编译,并行开发,提高开发和测试效率

15. 组件化

  1. 组件化是已复用为目的的
  2. 多个团队可能公用一个组件

16. SparseArray

  1. SparseArray通过两个数组实现,key为int型,value为Object型
  2. 适用于数据量较小时
  3. 通过二分法插入和删除数据

17. APP启动耗时

  1. 启动分为冷启动(包含初始化Application)和热启动,冷启动又可以分为第一次启动(会有额外的初始化数据库等操作)和不是第一次启动
  2. 开发时本地可以用adb命令获取启动时常:adb shell am start -w packagename/activity
  3. 线上可以在各个关键地方埋点统计时常

18. Android启动优化

  1. 启动优化的目的是提高用户感知的启动速度
  2. 可以采用TraceView等分析耗时,找出优化方向
  3. 优化的思路:
    1. 利用提前展示出来的Window,设置Theme,快速展示出来一个界面,给用户快速反馈的体验(启动后再改变回来)
    2. 异步初始化一些第三方SDK
    3. 延迟初始化
    4. 针对性的优化算法,减少耗时

19. 性能调优

  1. 性能调优包括很多方面:包括代码执行效率、网络、布局、内存、数据库、业务逻辑,打包速度等
  2. 需要针对具体问题具体分析,找到性能瓶颈:
  3. 善于借助工具,例如使用traceview跟踪,或者打点统计,profiler,leak canary等

网络优化

  1. 不用域名,用ip直连
  2. 请求合并与拆分
  3. 请求数据的缩小:删除无用数据,开启Gzip压缩
  4. 精简的数据格式:json/webp/根据设备和网络环境的不同采用不同分辨率图片
  5. 数据缓存
  6. 预加载
  7. 连接的复用:使用http2.0 (效率提升30%)
    扩展:JD Android客户端网络性能调优之HTTP/2协议升级

布局优化

  1. 合理的使用include/merge/viewstub等
  2. 减少布局层次,减少不必要的view和节点
  3. 使用布局缓存,避免重复inflate
    其他:可以用hierarchy viewer等工具进行分析

代码优化

  1. 降低执行时间:如优化算法和数据结构/利用多线程/缓存(包括对象缓存、线程池、IO 缓存、网络缓存等)/JNI/需求优化
  2. 同步改异步:例如使用surfaceview
  3. 错峰:提前或者延迟操作,比如启动中第三方sdk的加载

APK瘦身

  1. 分析APK
    1. 使用Android Studio的分析器分析apk结构
    2. 使用lint分析无用代码和资源
    3. 或者使用第三方工具,如NimbleDroid/ClassShark
  2. 删除无用资源
    1. 对无用资源和代码删除
    2. 优化结构,对重复资源去重
    3. 对依赖去重,依赖多个功能类似的sdk时,只保留一个
    4. 去除不需要的依赖,如语言support包可能包含多种语言,配置只保留我们需要的资源等
    5. 开启gradle的ProGuard/Code shrinking/minifyEnabled等,自动去除不需要的资源和代码】
  3. 压缩已用资源
    1. 选取适当的图片格式,如webp
    2. 对图片进行压缩
    3. 选取合适的依赖包,而不是直接依赖V7包等
    4. 使用微信的打包插件AndResGuard对图片进行压缩
    5. 使用facebook的ReDex对dex文件进行压缩
  4. 通过网络按需加载

20. 有什么提高编译速度的方法

  1. 提高电脑配置
  2. 优化Android studio配置
    1. 加大内存;
    2. 使用守护进程;
    3. 开启instant Run
    4. 使用离线gradle
    5. 使用新版的gradle
  3. 优化项目
    1. 使用第三方编译如[阿里的Freeline](https://www.freelinebuild.com/docs/zh_cn/###
    2. debug模式下可以不开启混淆等
    3. 模块化,多模块时使用aar包避免反复编译

21. 延迟加载的实现

  1. myHandler.postDelayed(mLoadingRunnable, DEALY_TIME);
 getWindow().getDecorView().post(new Runnable() {
    @Override
   public void run() {
        myHandler.post(mLoadingRunnable);
   }
  });
复制代码
  1. 监听MessageQueue,当空闲时执行任务
 // 拿到主线程的MessageQueue
 Looper.myQueue().addIdleHandler(new MessageQueue.IdleHandler() { 
      @Override public boolean queueIdle() { 
           // 在这里去处理你想延时加载的东西 
           delayLoad(); 
           
           // 最后返回false,后续不用再监听了。
           return false; 
           } 
  });

复制代码

22. ArrayMap

  1. ArrayMap 是Android中的一种用时间换空间的容器,用来替代HashMap
  2. 不适合大量数据或者有大量删除
  3. 采用二分查找
  4. 原理: 0. 使用两个数组进行存储
    1. 数组1储存key的HashCode
    2. 数组2储存key和value;value位置为key位置左移1位再加1
    3. hash冲突的解决:寻找下一个空位,因为查询时会比较hashcode和key,所以不会有问题

23. Bitmap没有必要主动recycle

Google Guide-manage bitmap manager

Google Guide-Cache Bitmap

/**
 * Free the native object associated with this bitmap, and clear the
 * reference to the pixel data. This will not free the pixel data synchronously;
 * it simply allows it to be garbage collected if there are no other references.
 * The bitmap is marked as "dead", meaning it will throw an exception if
 * getPixels() or setPixels() is called, and will draw nothing. This operation
 * cannot be reversed, so it should only be called if you are sure there are no
 * further uses for the bitmap. This is an advanced call, and normally need
 * not be called, since the normal GC process will free up this memory when
 * there are no more references to this bitmap.
 */
public void recycle() {
    if (!mRecycled && mNativePtr != 0) {
        if (nativeRecycle(mNativePtr)) {
            // return value indicates whether native pixel object was actually recycled.
            // false indicates that it is still in use at the native level and these
            // objects should not be collected now. They will be collected later when the
            // Bitmap itself is collected.
            mBuffer = null;
            mNinePatchChunk = null;
        }
        mRecycled = true;
    }
}

“*This will not free the pixel data synchronously; it simply allows it to be garbage collected if there are no other references.*”。
recycle确实不是立即回收对应bitmap的内存,因为不是synchronously

系统实现及原理


0. AsyncTask

  1. 为我们封装了thread和handler,并在内部实现了线程池,是一个轻量级的线程方案
  2. 缺点:不同版本实现方式可能不一样,例如线程池有可能是串行的(最新版本是并行的)
  3. 缺点:可定制化程度不高,例如我们不能很方便地cancel线程

1. AsyncTask是顺序执行么,for循环中执行200次new AsyncTask并execute,会有异常吗

  1. 不同版本的AsyncTask 不一样,最新版本中是串行的
  2. 不会,因为串行执行时用的是一个ArrayDeque来存放Runnable
  3. 扩展:AsyncTask内部有并行的ThreadPoolExecutor,储存队列用的是LinkedBlockingQueue,大小是128;如果我们并行执行,会抛出运行时异常

2. Android 渲染机制

  1. CPU对视图进行必要的计算:measure等
  2. 通过OpenGL 将CPU 处理过的数据交给GPU
  3. GPU进行栅格化并存入缓存
  4. Android 系统每隔16.6ms发出一个垂直同步信号,通知渲染

3. Android中Application和Activity的Context对象的区别

  1. 生命周期不一样
  2. Application 不能showDialog
  3. Application startActivity时必须new一个Task
  4. Application layoutInflate直接使用默认主题,可能与当前主题不一样

4. Zygote的启动过程

  1. 注册Zygote的socket监听端口,应用接收启动应用程序的消息
  2. 调用preload()方法加载系统资源,包括预加载类,Framework资源等
  3. 调用startSystemServer()方法启动SystemServer进程
  4. 调用runSelectLoop()方法进入监听和接收消息循环

5. Handler、Looper、Message、MessageQueue

它们的存在主要是为了线程间通信,通信的方式为:

  1. 在线程中调用Looper.prepare方法,生成一个looper与当前线程绑定(在生成looper的过程中,其构造方法在其内部创建了一个MessageQueue)
  2. 调用looper.loop方法,使当前线程循环读取MessageQueue中的message,并调用 msg.target.dispatchMessage方法,交由target处理(这里target是一个Handler实例)
  3. new 一个Handler,在创建Handler时,需要为他指定looper(若不指定则是当前线程的looper),Handler会取出looper中的MessageQueue也作为自己的MessageQueue。
  4. 调用Handler的sendMessage方法发送Message信息(这个方法将Message的target设置成当前handler,并把它加入到MessageQueue中)

6. 为什么主线程Looper.loop不会ANR

  1. ANR是应用程序无响应,原因是有事件在主线程运行时间过长造成新的事件无法处理,或者当前事件运行时间太长
  2. Looper.loop会循环处理到来的Message,当MessageQueue为空是,线程处于阻塞状态,释放cpu资源

7. Messenger 和 AIDL

都是进城间通信的方式,AIDL使用Binder通信,Messenger通过AIDL实现

Messenger原理:Handler内部持有一个IMessenger实例,IMssenger时一个aidl的接口,Messenger初始化时这个IMssenger实例也传给了Messenger

区别:1.Messenger是对AIDL的封装,使用简单;2.Messenger通过Handler处理传过来的Message,只能运行在一个线程中,我们在AIDL中可以运行多个线程;3.Messenger客户端调取服务方法的结果只能异步回调给客户端,AIDL可以同步回调

8. Binder

瞎jj写点凑个数吧

  1. Binder是Android中的一种进程间通信方式
  2. Binder通信是通过驱动来实现的,原理是在内核空间中进行内存映射,以为只有一次内存拷贝,所以速度较快
  3. Binder会验证权限,鉴定UID/PID来验证身份,保证了进程通信的安全性
  4. 系统的Service会想ServiceManager注册,使用时向ServiceManager获取
  5. Service/Client同ServiceManager通信的过程本身也是通过binder驱动实现的
  6. android中Service的bind通信是通过ActivityManagerService实现的
  7. Binder中的代理模式:
    1. 客户端代理对象和服务端实现统一接口
    2. 客户端获取服务端的引用,如果位于同一进程,那么获取的是服务端本身,如果是不同进程,获取到的是服务端的代理
    3. 通过代理请向服务端请求
    4. 服务端接收请求并处理,返回给客户端

9. AIDL

  1. 全称:Android Interface Define Language(Android接口定义语言)
  2. 目的是为了进行进程间通信
  3. 使用方式:定义aidl文件,编译器编译时会帮我们生成对应的JAVA代码;通过调用生成的java代码,来进行进程间通信
  4. 原理:通过Binder方式进行通信

10. Window、WindowManager以及Activity

  1. Window 0. Window有三类:系统Window、应用Window、子Window
    1. Window是接口,具体实现类是PhoneWindow
    2. Window 是一个抽象概念,我们并不能直接操作window
    3. Activity在创建的时候attach方法中会创建Window并使之与Activity关联
    4. Window中会创建Decorview,并通过ViewRootImpl与View交互
  2. WindowManager 0. 在Activity启动时,handleResumeActivity方法中启动activity的时候,会将主窗口加入到WindowManager中
    1. 我们并不能直接操作window,而是通过WindowManager
    2. WindowManagerImpl是其实现类,他将view增删改的操作交给 WindowManagerGlobal处理
    3. WindowManagerGlobal 中会调用 ViewRootImpl的方法
    4. ViewRootImpl通过IWindowSession与WindowManagerService交互

11. 理解Window和WindowManager

  1. Window用于显示View和接收各种事件,Window有三种类型:应用Window(每个Activity对应一个Window)、子Window(不能单独存在,附属于特定Window)、系统window(Toast和状态栏)
  2. Window分层级,应用Window在1-99、子Window在1000-1999、系统Window在2000-2999.WindowManager提供了增删改View三个功能。
  3. Window是个抽象概念:每一个Window对应着一个View和ViewRootImpl,Window通过ViewRootImpl来和View建立联系,View是Window存在的实体,只能通过WindowManager来访问Window。
  4. WindowManager的实现是WindowManagerImpl其再委托给WindowManagerGlobal来对Window进行操作,其中有四个List分别储存对应的View、ViewRootImpl、WindowManger.LayoutParams和正在被删除的View
  5. Window的实体是存在于远端的WindowMangerService中,所以增删改Window在本端是修改上面的几个List然后通过ViewRootImpl重绘View,通过WindowSession(每个应用一个)在远端修改Window。
  6. Activity创建Window:Activity会在attach()中创建Window并设置其回调(onAttachedToWindow()、dispatchTouchEvent()),Activity的Window是由Policy类创建PhoneWindow实现的。然后通过Activity#setContentView()调用PhoneWindow的setContentView。

12. Window添加的过程

  1. ActivityThread做为APP的入口,会执行 handleLaunchActivity -> performLaunchActivity
  2. performLaunchActivity中通过ClassLoader创建Activity对象 -> 调用Activity.attach方法 -> callActivityOnCreate
  3. 在Activity.attach方法中会创建一个Window实例PhoneWindow,并将activity作为callback传递给window
  4. 在Activity的onCreate方法中,会调用setContentView,setContentView会调用PhoneWindow 的 setContentView
  5. PhoneWindow 的 setContentView会创建DecorView,并把我们自己设置的ContentView和DecorView绑定
  6. performLaunchActivity至此走完,之后的performResumeActivity会调用handleResumeActivity方法
  7. 在handleResumeActivity中,会调用Activity的getWindowManager()获取一个WindowManager,接着调用WindowManager的addView方法
  8. addView实际执行的是WindowManagerGlobal的addView(),这里会创建一个ViewRootImpl,并调用ViewRootImpl的setView方法
  9. 在setView这个方法内部,会通过跨进程的方式向WMS(WindowManagerService)发起一个调用,从而将DecorView最终添加到Window上

13. APK的安装流程

img

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

14. 双亲委托模式类加载的过程

  1. 调用当前类加载器的loadClass加载类
  2. loadClass中先调用findLoadedClass查看类是否已加载,已加载->over
  3. 还没有加载,调用父类加载器的loadClass,父类加载器加载完成 -> over
  4. 如果父类加载器没有加载,调用本类加载器的findClass,并在findClass中调用·defineClass方法加载

15. Android中ClassLoader

  1. Android中加载的是Dex文件
  2. ClassLoader 是个抽象类,其具体实现的子类有 BaseDexClassLoader 和SecureClassLoader,(SecureClassLoader 的子类是 URLClassLoader ,其只能用来加载 jar 文件,这在 Android 的 Dalvik/ART 上没法使用的)
  3. BaseDexClassLoader 的子类是 PathClassLoader 和 DexClassLoader 。
  4. PathClassLoader 在应用启动时创建,从 data/app/… 安装目录下加载 apk 文件。
  5. DexClassLoader 则没有此限制,可以从 SD 卡或网络加载包含 class.dex 的 .jar 和 .apk 文件,这也是插件化和热修复的基础

项目构建


0. 点击AndroidStudio的build按钮后发生了什么

build过程即执行gradle task 打包生成apk的过程:

  1. 通过appt工具,将资源文件生成R.java文件;将aild文件转换成对应的java文件
  2. 编译java文件,生成.class文件
  3. 将.class文件转换成Android虚拟机支持的.dex文件
  4. 通过apkbuilder将dex文件和编译后的资源文件生成apk文件
  5. 对apk进行签名和对齐

1. Android Debug和Release状态的不同

调试模式允许我们为优化代码而添加许多额外的功能,这些功能在Release时都应该去掉;Release包可以为了安全等做一些额外的优化,这些优化可能比较耗时,在Debug时是不需要的

  1. log日志只在debug时输入,release时应该关掉(为了安全)
  2. 签名/混淆/压缩等在debug编译时可以加入,减少打包时间
  3. 可以在debug包中加入一些额外的功能辅助我们开发,如直接打印网络请求的控件,内存泄漏检测工具LeakCanary等
  4. 在打Release包时,除了混淆等操作,往往还需要加固操作,保证APP的安全

2. Dalvik和Art

  1. Dalvik 是 Android 中使用的虚拟机,执行dex字节码
  2. Dalvik 与 JVM 相比
    1. JVM执行class字节码文件,Dalvik执行dex字节码文件,dex文件做了优化,提及更小
    2. Dalvik是基于寄存器的,VM基于栈
  3. Art是Dalvik虚拟机的升级版,Dalvik是解释型的,Art是翻译型的
    1. Dalvik虚拟机执行的是dex字节码,ART虚拟机执行的是本地机器码
    2. 相对于Dalvik,Art安装占用内存更大,安装时间更长,但是运行速度会有提升

3. 解决方法数超过65535的方法

  1. 代码混淆(原理是减小方法数)
  2. sdk裁减(原理是减小方法数)
  3. multi-dex(原理是打包成多个dex)

4. multi-dex 问题

引入multi-dex后,在5.0以下手机上,第一次安装后启动速度可能变慢甚至anr,需要进行优化:如单独开一个线程;修改keep文件等

5. App签名

  1. 原理:先计算出hash值,再对hash值进行非对称加密
  2. V1版本签名生成的APK中与签名有关的文件:
    1. MANIFEST.MF: jar 包的文件清单,在 apk 中我们用来记录所有非目录文件的 数据指纹
    2. CERT.SF:根据MANIFEST.MF生成的文件
    3. CERT.RSA:这里会把之前生成的CERT.SF文件,用私钥计算出签名, 然后将签名以及包含公钥信息的数字证书一同写入CERT.RSA 中保存
  3. V1版本存在安全漏洞,google推出了v2版

6. 常用的adb 和 adb shell命令

查看当前连接的设备:adb devices
结束adb连接: adb kill-server
安装apk: adb install test.apk
从手机获取文件和推送文件到手机:adb push <本地文件> <远程路径> ; adb pull <远程路径> <本地路径>
获取log信息:adb logcat > log.txt

启动Activity: adb shell am start -n 包名/包名+类名
显示系统Activity栈信息:adb shell dumpsys activity
发送广播:adb shell am broadcast -a “android.intent.action.AdupsFota.WriteCommandReceiver”
查看进程信息:adb shell ps <package_name|PID>
杀掉某个进程:adb shell kill pidNumber
查看内存占用:adb shell dumpsys meminfo <package_name|PID>

7. jar和aar的区别

Jar包里面只有代码,aar里面不光有代码还包括代码还包括资源文件,比如 drawable 文件,xml 资源文件。对于一些不常变动的 Android Library,我们可以直接引用 aar,加快编译速度

8. 不同的CPU架构对APP的影响

  1. cpu的架构有armeabi、armeabi-v7a、x86等
  2. 针对不同的CPU,使用不同的so包,可以使性能最大化
  3. 如果a.so提供了armeabi、armeabi-v7a、x86格式,那么b.so也要提供这几个格式,否则可能崩溃
  4. 当没有对应cpu的so时,会选择其他so,但执行速度会变慢:当一个应用安装在设备上,只有该设备支持的CPU架构对应的.so文件会被安装。在x86设备上,libs/x86目录中如果存在.so文件的话,会被安装,如果不存在,则会选择armeabi-v7a中的.so文件,如果也不存在,则选择armeabi目录中的.so文件(因为x86设备也支持armeabi-v7a和armeabi)

9. compileSdkVersion,minSdkVersion,targetSdkVersion 都是啥

  1. compileSdkVersion :编译所依赖的版本,它可以让我们在写代码时调用最新的api,告知我们过时的api
  2. minSdkVersion:最小的可安装此App的版本,意味着我们不用做低于此版本的兼容
  3. targetSdkVersion: 目标版本,可以让我们虽然运行在最新的手机上,但是行为和target版本一致,比如:如果targetSdkVersion小于Android 6.0,那么即使我们的app运行在6.0系统上,也不需要运行时权限

10. 低版本SDK实现高版本api

  1. 使用@TargetApi 来标明api版本,这样编译器就不会报错了
  2. 在代码逻辑中判断版本,在低版本上调用替代api或自己实现的算法。

部分功能的实现


0. 怎样退出终止自己的APP

  1. 记录启动的activity
  2. 需要退出时调用存活activity的finish方法,并调用System.exit(0)方法

1. Android中启动线程的几种方式

  1. java中可以用实现Runnable接口、实现Callable接口、继承Thread类三种方式
  2. Android中还可以用AsyncTaskHandlerThreadIntendService

2. 长链接+心跳包

  1. 长连接:App 与服务器建立一个生命周期很长的连接,服务器通过push向App推送消息
  2. 心跳包:App 每隔一段时间就会向服务器查询是否有新的消息
  3. 长连接可能因为各种原因被打断,心跳包接收消息可能不及时,所以我们可以采取长连接+心跳包的方式:通过Socket建立一个长连接,并通过心跳包检测这个长连接是否存活,长连接中断的话则重新建立

3. Android 中XML的解析

Androi中主要有DOM,SAX,PULL三种方式
DOM将文件都加载到内存中,比较消耗内存;SAX和PULL节省内存,PULL使用比SAX更简单

4. 系统上安装了多种浏览器,能否指定某浏览器访问指定页面?请说明原由。

  1. 指定浏览器:intent.setClassName(“com.android.browser”,”com.android.browser.BrowserActivity”);
  2. 指定网址: Uri uriBrowsers = Uri.parse(“http://www.sina.com.cn”); intent.setData(uriBrowsers);

5. java中如何引用本地语言

可以用JNI(java native interface java 本地接口)接口

6. JNI的使用方式

  1. 下载NDK,配置环境变量,配置gradle文件

  2. JAVA中声明native 方法如private native String printJNI(String inputStr);

  3. 生成或写对应的头文件

  4. 编写对应文件实现代码

  5. 编译成so文件

  6. 使用

  7. 扩展:

    native调用java代码

    1. 获取你需要访问的Java对象的类
jclass cls = (*env)->GetObjectClass(env, obj);       //使用GetObjectClass方法获取obj对应的jclass。   
class cls = (*env)->FindClass(“android/util/log”) //直接搜索类名,需要是static修饰的类。  
复制代码
  1. 获取MethodID:
methodID mid = (*env)->GetMethodID(env, cls, "callback", "(I)V"); //GetStaticMethodID(…),获取静态方法的ID使用GetMethdoID方法获取你要使用的方法的MethdoID  
复制代码
  1. 调用:
    (*env)->CallVoidMethod(env, obj, mid, depth);// CallStaticIntMethod(….) , 调用静态方法  
复制代码

7. NDK是什么

NDK 是Native Development Kit 的缩写,是一些列工具的集合,帮助开发者迅速的开发C/C++的动态库

8. 什么是JVM

  1. JVM是JAVA虚拟机,保证了java语言的跨平台性,是编译后的 Java 程序(.class文件)和硬件系统之间的接口
  2. JVM = 类加载器 classloader + 执行引擎 execution engine + 运行时数据区域 runtime data area

9. 视频加密

视频加密根据场景的不同有很多种方式

  1. 如仅对地址加密,可以起到防盗链的目的,可以与其他方法一起使用
  2. 对整个文件加密,加解密时间长,不实用
  3. 对文件的头中尾加密,播放器可以直接跳过,破解简单,不实用
  4. 对视频流加密(基于苹果HLS协议的加密 基于RTPE协议)
  5. 关键帧加密

10. 绘制印章

  1. 创建一个bitmap,拿到canvas
  2. 在canvas上绘制圆,绘制五角星,绘制文字,返回bitmap

11. 文字阴影和描边的实现

  1. 阴影:shadow属性
  2. 描边:两个TextView叠加;或者重写onDraw方法

12. Android生成设备唯一标识符

选取 DeviceId,AndroidId,Serial Number,Mac,蓝牙地址等中的一个或者几个作为种子,生成UUID。

13. 实现客户端的自动登录

  1. 第一次登录时保存两个token,一个长效一个短效
  2. 短效token用于每次网络请求的用户标识
  3. 长效token用于当短效token失效时自动登录,重新获取token

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

使用BitmapRegionDecoder

15. SSL证书的验证

  1. 在使用Https时,我们需要对SSL证书做验证以确保有效

  2. 证书需要验证证书有效性,时效,域名等

  3. Android中WebView可以重写onReceivedSslError方法来处理ssl证书不对时的情况

  4. OkHttp设置证书验证

    1. 验证可以是双向的,也可以是单向的
    2. 单向:将服务器对应的Server.cer文件打包进Apk中,通过cer文件生成SSLSocketFactory,并将其设置给okHttpClient
    3. 双向:用Server.cer和Client.key生成SSLSocketFactory,并将其设置给okHttpClient

16. 计算一张100px*100px的图片在内存中会占用多大内存

  1. 内存大小 = 100100像素点大小
  2. 像素点大小和编码方式有关:ARGB_8888占8+8+8+8=32bit;ARGB_4444占4+4+4+4 = 16bit;

17. 如何实现动态代理

  1. 创建一个实现InvocationHandler接口的类,它必须实现invoke方法
  2. 调用Proxy的静态方法newProxyInstance,创建一个代理类

18. Android中有哪些方法实现定时和延时任务?它们的适用场景是什么?

  1. 倒计时类:用CountDownTimer
  2. 延迟类: 1. CountDownTimer,可巧妙的将countDownInterval设成和millisInFuture一样,这样就只会调用一次onTick和一次onFinish 2. handler.sendMessageDelayed,可参考CountDownTimer的内部实现,简化一下,个人比较推荐这个 3. TimerTask,代码写起来比较乱 4. Thread.sleep,感觉这种不太好
  3. 定时类: 1. 参照延迟类的,自己计算好要延迟多少时间 2. handler.sendMessageAtTime 3. AlarmManager,适用于定时比较长远的时间,例如闹铃

概念


0. Json有什么优势

有比较才会有优势,我们通常将Json与Xml进行比较,Json更加轻量。我觉得在某些程度上讲,这是一个仁者见仁智者见智的问题。例如有些人认为Json相比Xml更易读,有些人责认为不然,这里大致列举几条,仅供参考

  1. 结构简单,可读性更强,读写更加容易
  2. 格式是压缩的,占用带宽小
  3. 支持多种语言
  4. 因为JSON格式能够直接为服务器端代码使用,大大简化了服务器端和客户端的代码开发量

1. MVC

MVC是model,view,controller的缩写

  1. 模型(model)对象:是应用程序的主体部分,所有的业务逻辑都应该写在该层;
  2. 视图(view)对象(对应Android中的布局xml文件):是应用程序中负责生成用户界面的部分。也是在整个mvc架构中用户唯一可以看到的一层,接收用户的输入,显示处理结果; 
  3. 控制器(control)对象(对应Android中的Activity):是根据用户的输入,控制用户界面数据显示及更新model对象状态的部分,控制器更重要的一种导航功能,响应用户出发的相关事件,交给m层处理。 扩展:和MVP最大的区别是View和Model可以直接交互

2. 什么是控制反转(IOC Inversion of Control)

  1. 在Java开发中,IoC意 味着将你设计好的类交给系统去控制,而不是在你的类内部控制
  2. 是框架的重要特征
  3. Android中Activity 的生命周期都是框架控制的,是一种控制反转

3. Android中的IOC(控制反转)框架

  1. 控制反转:将对象的创建交给框架去做
  2. 常用的框架:ButterNife/Android Annotation
  3. 2中两个框架都是通过java的注释框架实现的,并且都是作用在编译期

4. 请解释下Android程序运行时权限与文件系统权限的区别

  1. 运行时权限是APP启动后由Android虚拟机(如Dalvik)控制的
  2. 文件系统权限是Linux内核授权

5. AOP 面向切面编程

  1. 面向切面编程是通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术
  2. 例如很多功能需要先登陆,登陆在这里就是一个切面。

6. MVP 架构中 Presenter 定义为接口有什么好处

  1. 在goole官方的demo中,通过一个Contract把将View和Presenter管理起来,强化其一一对应的关系,便于操作
  2. [但是也有人认为不应该将Presenter定义为接口]http://www.infoq.com/cn/articles/do-not-give-the-prensenter-in-mvp-interface),因为这并不会方便测试,还会增加复杂性

其他


0. Android中的几种动画

  1. 普通动画(视图动画、补间动画)
  2. 属性动画
  3. 帧动画
  4. 物理动画(Android 8.0)

1. HttpClient和HttpURLConnection的区别

HttpClient

  1. Apache公司提供的库
  2. 拥有丰富的API,但也因为这个原因,在不破坏兼容性的前提下,其庞大的API也使人难以改进
  3. Android 6.0中抛弃了Http Client,替换成OkHttp

HttpURLConnection

  1. Sun公司提供的库
  2. 功能比较简单,可拓展性强
  3. 直接支持GZIP压缩,并且在Android 4.0 以上支持cache缓存,提高了网络效率

2. Intent

  1. Intent是Android中的信使,可以启动Activity,Service等
  2. Intent可以设置的几项值:Action, Category, Data/Type,Component
  3. 当设定Component时,是显式调用,其余是隐式调用

3. IntentFilter

  1. IntentFilter 在AndroidMainifest中注册,用来帮助系统选出用户定义的隐式Intent对应的Activity /Service等
  2. Android是通过Intent的action、data、category这三个属性来进行匹配判断的
    1. action:隐式启动需要给Intent设置Action,如果没有设置这条匹配则自动通过;必须给IntentFilter设置一个action
    2. data:如果Intent没有提供type,系统将从data中得到数据类型。同action类似,只要Intent的data只要与Intent Filter中的任一个data声明完全相同,data方面就完全匹配成功。
    3. category:Intent的Category可以有多个,每一个都需要和IntentFilter匹配才能算匹配上,不设置Intent的category时是默认的(DEFAULT)
    4. 总结:只有一个Intent的action、data、category匹配上IntenFilter中的一组数据时,才算匹配成功。

4. Intent/Bundle支持传送哪种类型的数据

  1. 基本类型及其数组
  2. 实现了Serializable或者Parcelable的类型及其数组

5. Manifest.xml文件中主要包括哪些信息

  1. manifest:根节点,描述了包名,版本号等。
  2. application:包含package中application级别组件声明的根节点。
  3. activity:Activity是用来与用户交互的主要工具。
  4. receiver:IntentReceiver能使的application获得数据的改变或者发生的操作,即使它当前不在运行。
  5. service:Service是能在后台运行任意时间的组件。
  6. provider:ContentProvider是用来管理持久化数据并发布给其他应用程序使用的组件。
  7. uses-permission:请求你的package正常运作所需赋予的安全许可。
  8. permission: 声明了安全许可来限制哪些程序能你package中的组件和功能。
  9. uses-feature:使用到的硬件信息,如nfc
  10. upports-screens:支持的屏幕类型
  11. meta-data:data数据
  12. instrumentation:声明了用来测试此package或其他package指令组件的代码。

6. dp, dip, dpi, px, sp是什么意思

  1. dp = dip(device independent pixels),是设备独立像素
  2. sp:scaled pixels(放大像素),主要用于字体显示。
  3. px(pixel):像素
  4. dpi(dot per inch)

7. dp与px的换算公式

  1. px = dp*像素密度/160
 public static int dp2px(Context context, float dpValue) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (int) (dpValue * scale + 0.5f);
    }

复制代码

8. layout-sw400dp, layout-w400dp分别代表什么意思

  1. layout-w400dp:当屏幕相对宽度大于400dp时,来这里取layout(与横竖屏有关)
  2. layout-sw400dp:当屏幕绝对宽度大于400dp时,来这里取layout(与横竖屏无关)

9. Android 样式和主题

  1. 样式(Styles):可以理解成是针对View或者窗口(Window)设置外观或者格式的一个属性集合
  2. 主题(Themes):主题相比单个视图而言,是应用到整个 Activity 或者 application 的样式
  3. 区别:
    1. Theme作用域是Activity或者Application,Stytle针对View或者窗口(Window)
    2. 某些主题样式不可以在View中使用,例如”@android:style/Theme.NoTitleBar” 等 扩展: 属性(Attributes):你也可以将单个属性应用到 Android 样式上,通常会在自定义View 的时候,自定义属性。

10. Android P的新特性(2018/5/5)

  1. Goole 下个Android版本的预览已经放出,代号p
  2. 支持wifi室内定位
  3. 适配刘海屏
  4. 通知栏改进:可以显示对话,附加照片和表情等
  5. 多摄像头API
  6. 神经网络API 1.1

11. 热修复原理

热修复的原理是让我们的新类替换掉原来类的加载,从而达到修复的目的,以下是一种思路:

  1. java中通过PathClassLoader和DexClassLoader来加载类,类加载的方式是双亲委派模式
  2. PathClassLoader和DexClassLoader都继承自BaseDexClassLoader
  3. BaseDexClassLoader中维护了一个dex的数组
  4. 我们可以通过DexClassLoader加载类,然后通过反射的机制将加载进来的数组添加到path数组的前面
  5. 加载的时候找到我们需要的class后,就不再继续向后找了,所以可以达到修复的目的

12. Android中的进程间通信(IPC)

  1. Bundle : 只支持四大组件
  2. 文件共享:不适合并发
  3. Messenger:封装了AIDL
  4. AIDL:通过binder实现
  5. ContentProvider:共享数据
  6. Socket:适用于网络等

13. 解决Android7.0 更新安装包时不能自动安装问题

  1. Android 7.0中私有目录访问会被限制,导致不能自动安装
  2. 可以使用FileProvider来解决

如何开启多进程?应用是否可以开启N个进程?

  1. 通过在AndroidManifest中给Activity设置process属性开启新的进程
  2. 可以开启N个进程,例如给webview单独开启一个进程,但要处理多进程间通信和多次初始化Handler问题

Service先start再bind如何关闭service

先unbind,再stop

为什么bindService可以跟Activity生命周期联动

  1. 在Activity退出时调用unbind方法,service会销毁
  2. 如果不调用unbind方法,service也会销毁,但是会抛出leaked serviceConnection 异常 (参考2)

子线程中如何使用Handler

  1. 使用HandlerThread,新建Handler时通过调用HandlerThread 的 getLooper方法拿到looper
  2. 原理:HandlerThread在run时会为我们生成一个looper,getLooper方法会阻塞等待直到 looper!=null 才返回。

如何进行单元测试

  1. Junit:不需要依赖android环境,适合于逻辑测试
  2. Instrumentation:依赖android环境,可以启动Activity,模拟内存回收,获取组件等,模拟点击等。需要在AndroidManifest中进行配置,适合于更复杂的测试

TabLayout如何设置指示器的宽度

通过反射拿到对应的指示器,设置LayoutParams

Android中如何查看一个对象的回收情况

  1. 外部:通过adb shell 命令导出内存,借助工具分析
  2. 内部:通过将对象加入WeakReference,配合RefernceQueue观察对象是否被回收,被回收的对象会被加入到RefernceQueue中

回形打印二维数组

思路:递归实现,分别打印每一圈

APK内容

img

class文件如何转化成dex

使用build tools 下的dx工具

class 和dex文件对比:
1. 都是二进制文件
2. class文件存在容与,dex文件将整个工程的类信息整合到了一起,去掉了冗余
复制代码

硬件加速

  1. 随便写点凑个数吧=-=
  2. 硬件加速的四个级别:Application/Activity/Window/View

为什么选择Binder作为通信方式

  1. binder效率更高:socket是一个通用接口,效率低;管道和队列内存拷贝两次,效率低;共享内存控制复杂
  2. binder更加安全:binder可以建立私有通道,通过uid/pid验证身份

LeakCanary


  1. 在Application中注册一个ActivityLifecycleCallbacks来监听Activity的销毁
  2. 通过IdleHandler在主线程空闲时进行检测
  3. 检测是通过WeakReference实现的,如果没有被回收会再次调用gc再确认一遍
  4. 确认有泄漏后,dump hprof文件,并开启一个进程IntentService通过HAHA进行分析

OkHttp(基于3.9版本)


使用

1. 在gradle中添加依赖

compile 'com.squareup.okhttp3:okhttp:3.9.0'
compile 'com.squareup.okio:okio:1.13.0'
复制代码

2. 创建OkHttpClient,并对timeout等进行设置

File sdcache = getExternalCacheDir();
int cacheSize = 10 * 1024 * 1024;
OkHttpClient.Builder builder = new OkHttpClient.Builder()
        .connectTimeout(15, TimeUnit.SECONDS)
        .writeTimeout(20, TimeUnit.SECONDS)
        .readTimeout(20, TimeUnit.SECONDS)
        .cache(new Cache(sdcache.getAbsoluteFile(), cacheSize));
OkHttpClient mOkHttpClient=builder.build();
复制代码

3. 创建Request

  • get请求
Request request = new Request.Builder()
            .url("http://www.baidu.com")
            .build();
复制代码
  • post请求(post需要传入requsetBody)
RequestBody formBody = new FormEncodingBuilder()
            .add("size", "10")
            .build();
    Request request = new Request.Builder()
            .url("http://api.1-blog.com/biz/bizserver/article/list.do")
            .post(formBody)
            .build();
复制代码

4. 创建Call并执行(okHttp的返回结果并没有在ui线程)

Call call = mOkHttpClient.newCall(request);
复制代码
  • 同步执行
Response mResponse=call.execute();
        if (mResponse.isSuccessful()) {     
           return mResponse.body().string();
       } else {
           throw new IOException("Unexpected code " + mResponse);
       }
复制代码
  • 异步执行
call.enqueue(new Callback() {
        @Override
        public void onFailure(Request request, IOException e) {
        }
        @Override
        public void onResponse(Response response) throws IOException {
            String str = response.body().string();
            Log.i("wangshu", str);
            runOnUiThread(new Runnable() {
                @Override
                public void run() {
                    Toast.makeText(getApplicationContext(), "请求成功", Toast.LENGTH_SHORT).show();
                }
            });
        }
    });
复制代码

4. 封装

因为以下原因,所以我们需要封装:

  • 避免重复代码编写
  • 请求的回调改为UI线程
  • 其他需要的逻辑:例如加解密等

OkHttp中的设计模式

  1. Builder模式:OkHttpClient 和Request等都是通过Builder模式创建的
  2. 责任链模式:拦截器通过责任链模式进行工作
  3. 门面模式:整体采用门面模式,OkHttpClient为门面,向子系统委派任务
  4. 享元模式:连接池等采用了享元模式
  5. 其他:工厂模式、代理模式等

源码分析

1. Call

  • Call的实现类为RealCall
  • 在执行execute或者enqueue时,会取出okHttpClient中的Dispatcher执行对应的方法
client.dispatcher().enqueue(new AsyncCall(responseCallback, forWebSocket));
复制代码

2. Diapatcher

  • Diapatcher在OkHttpClient build时进行初始化
  • Dispatcher负责进行任务调度,内部维护一个线程池,处理并发请求
  • Dispatcher内部有三个队列
/** 将要运行的异步请求队列 */
private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>();
/**正在运行的异步请求队列 */
private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>();
/** 正在运行的同步请求队列 */
private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>();
复制代码
  • 执行时,线程会调用AsyncCall的excute方法

3. AsyncCall

  • AsyncCall是RealCall的一个内部类,实现了Runnalbe接口
  • AsyncCall 通过 getResponseWithInterceptorChain方法取得Response
  • 执行完毕后通过client.dispatcher().finished(this);将自身从dispatcher队列中取出,并取出下一个加入相应队列
//AsyncCall 的excute方法
@Override protected void execute() {
  boolean signalledCallback = false;
  try {
    Response response = getResponseWithInterceptorChain(forWebSocket);
    if (canceled) {
      signalledCallback = true;
      responseCallback.onFailure(RealCall.this, new IOException("Canceled"));
    } else {
      signalledCallback = true;
      responseCallback.onResponse(RealCall.this, response);
    }
  } catch (IOException e) {
    if (signalledCallback) {
      // Do not signal the callback twice!
      logger.log(Level.INFO, "Callback failure for " + toLoggableString(), e);
    } else {
      responseCallback.onFailure(RealCall.this, e);
    }
  } finally {
    client.dispatcher().finished(this);
  }
}

复制代码

4. getResponseWithInterceptorChain

getResponseWithInterceptorChain是用责任链的方式,执行拦截器,对请求和请求结果进行处理

  • getResponseWithInterceptorChain 中创建拦截器,并创建第一个RealInterceptorChain,执行其proceed方法
Response getResponseWithInterceptorChain() throws IOException {
    // Build a full stack of interceptors.
    List<Interceptor> interceptors = new ArrayList<>();
    interceptors.addAll(client.interceptors());
    interceptors.add(retryAndFollowUpInterceptor);
    interceptors.add(new BridgeInterceptor(client.cookieJar()));
    interceptors.add(new CacheInterceptor(client.internalCache()));
    interceptors.add(new ConnectInterceptor(client));
    if (!forWebSocket) {
      interceptors.addAll(client.networkInterceptors());
    }
    interceptors.add(new CallServerInterceptor(forWebSocket));

    Interceptor.Chain chain = new RealInterceptorChain(interceptors, null, null, null, 0,
        originalRequest, this, eventListener, client.connectTimeoutMillis(),
        client.readTimeoutMillis(), client.writeTimeoutMillis());

    return chain.proceed(originalRequest);
  }
复制代码
  • RealInterceptorChain的proceed方法中,会取出拦截器,并创建下一个Chain,将其作为参数传给拦截器的intercept方法
  // If there's another interceptor in the chain, call that.
  if (index < client.interceptors().size()) {
    Interceptor.Chain chain = new ApplicationInterceptorChain(index + 1, request, forWebSocket);
    //从拦截器列表取出拦截器
    Interceptor interceptor = client.interceptors().get(index);
    Response interceptedResponse = interceptor.intercept(chain);

    if (interceptedResponse == null) {
      throw new NullPointerException("application interceptor " + interceptor
          + " returned null");
    }

    return interceptedResponse;
  }

  // No more interceptors. Do HTTP.
  return getResponse(request, forWebSocket);
}

复制代码

拦截器

1. 自定义拦截器

  • 自定义拦截器分为两类,interceptor和networkInterceptor(区别:networkInterceptor处理网络相关任务,如果response直接从缓存返回了,那么有可能不会执行networkInterceptor)
  • 自定义方式:实现Interceptor,重写intercept方法,并注册拦截器

2. 系统拦截器

  • RetryAndFollowUpInterceptor:进行失败重试和重定向
  • BridgeInterceptor:添加头部信息
  • CacheInterceptor:处理缓存
  • ConnectInterceptor:获取可用的connection实例
  • CallServerInterceptor:发起请求

连接池复用

在ConnectInterceptor中,我们获取到了connection的实例,该实例是从ConnectionPool中取得

1. Connection

  • Connection 是客户端和服务器建立的数据通路,一个Connection上可能存在几个链接
  • Connection的实现类是RealConnection,是socket物理连接的包装
  • Connection内部维持着一个List引用

2. StreamAllocation

StreamAllocation是Connection维护的连接,以下是类内注解

 <ul>
 *     <li><strong>Connections:</strong> physical socket connections to remote servers. These are
 *         potentially slow to establish so it is necessary to be able to cancel a connection
 *         currently being connected.
 *     <li><strong>Streams:</strong> logical HTTP request/response pairs that are layered on
 *         connections. Each connection has its own allocation limit, which defines how many
 *         concurrent streams that connection can carry. HTTP/1.x connections can carry 1 stream
 *         at a time, HTTP/2 typically carry multiple.
 *     <li><strong>Calls:</strong> a logical sequence of streams, typically an initial request and
 *         its follow up requests. We prefer to keep all streams of a single call on the same
 *         connection for better behavior and locality.
 * </ul>
复制代码

3. ConnectionPool

ConnectionPool通过Address等来查找有没有可以复用的Connection,同时维护一个线程池,对Connection做回收工作

Retrofit


Retrofit帮助我们对OkHttp进行了封装,使网络请求更加方便

使用

1. 添加依赖

dependencies {
    compile 'com.squareup.retrofit2:retrofit:2.0.2'
  }
复制代码

2. 创建Retrofit实例

Retrofit retrofit = new Retrofit.Builder() 
 .baseUrl("http://fanyi.youdao.com/") // 设置网络请求的Url地址
 .addConverterFactory(GsonConverterFactory.create()) // 设置数据解析器 
 .addCallAdapterFactory(RxJavaCallAdapterFactory.create()) // 支持RxJava平台 .build();
复制代码

3. 创建网络接口

@GET("user")
Call<User> getUser(@Header("Authorization") String authorization)
复制代码

4. 创建Call

 GetRequest_Interface request = retrofit.create(GetRequest_Interface.class);
//对 发送请求 进行封装
Call<Reception> call = request.getCall();
复制代码

5. 执行Call的请求方法

//发送网络请求(异步) call.enqueue(new Callback<Translation>() { 
//请求成功时回调
 @Override 
public void onResponse(Call<Translation> call, Response<Translation> response) { 
   //请求处理,输出结果
    response.body().show(); 
 } 
 //请求失败时候的回调 
 @Override 
 public void onFailure(Call<Translation> call, Throwable throwable) { 
     System.out.println("连接失败"); 
 } 
 });
 
 // 发送网络请求(同步) Response<Reception> response = call.execute();

复制代码

源码解析

1. Retrofit

Retrofit 通过builder模式创建,我们可以对其进行各种设置:

  • baseUrl:请求地址的头部,必填
  • callFactory:网络请求工厂(不进行设置的话默认会生成一个OkHttpClient)
  • adapterFactories:网络请求适配器工厂的集合,这里有适配器因为Retrofit不仅支持Android,还支持Ios等其他平台(不进行设置的话会根据平台自动生成)
  • converterFactories:数据转换器工厂的集合(将网络返回的数据转换成我们需要的类)
  • callbackExecutor:回调方法执行器(Android平台默认通过Handler发送到主线程执行)

2. Call

我们的每个method对应一个Call, Call的创建分为两步:

  • retorfit.create(myInfterfaceClass.class)创建我们网络请求接口类的实例
  • 调用对应方法拿到对应网络请求的Call

关键在第一步,第一步是通过动态代理实现的

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);//1
          OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args);
          return serviceMethod.callAdapter.adapt(okHttpCall);
        }
      });
}
复制代码
  • 通过loadServiceMethod方法生成mehtod对应的ServiceMethod
  • 将ServiceMethod和方法参数传进OkHttpCall生成OkHttpCall
  • 调用callAdapter方法对OkHttpCall进行处理并返回
1. ServiceMethod

loadServiceMethod方法会首先在缓存里查找是否有该method对应的ServiceMethod,没有的话调用build方法创建一个

ServiceMethod loadServiceMethod(Method method) {
 ServiceMethod result; 
 // 设置线程同步锁 
 synchronized (serviceMethodCache) { 
 result = serviceMethodCache.get(method);
  // ServiceMethod类对象采用了单例模式进行创建 
  // 即创建ServiceMethod对象前,先看serviceMethodCache有没有缓存之前创建过的网络请求实例 
  // 若没缓存,则通过建造者模式创建 
  serviceMethod 对象 if (result == null) { 
  // 下面会详细介绍ServiceMethod生成实例的过程 
  result = new ServiceMethod.Builder(this, method).build(); 
  serviceMethodCache.put(method, result); 
   } 
  }
   
  return result;
}

复制代码

ServiceMethod的创建过程即是对method的解析过程,解析过程包括:对注解的解析,寻找合适的CallAdapter和Convert等

2. OkHttpCall

OkHttpCall实现了Call接口,当执行excute或enqueue请求命令时,内部通过传入的CallFactory(OkHttpClient)执行网络请求

3. callAdapter

如果我们没有对CallAdapter进行设置,它的值将是Android平台的默认设置,其adapt方法如下

public <R> Call<R> adapt(Call<R> call) { 
    return new ExecutorCallbackCall<>(callbackExecutor, call); 
} 


ExecutorCallbackCall(Executor callbackExecutor, Call<T> delegate) {

 this.delegate = delegate; 
 // 把上面创建并配置好参数的OkhttpCall对象交给静态代理delegate 
 // 静态代理和动态代理都属于代理模式 
 // 静态代理作用:代理执行被代理者的方法,且可在要执行的方法前后加入自己的动作,进行对系统功能的拓展 
 
 this.callbackExecutor = callbackExecutor; 
 // 传入上面定义的回调方法执行器 
 // 用于进行线程切换 }

复制代码

ExecutorCallbackCall对OkHttpCall进行了装饰,会调用CallBackExcutor对OkHttpCall执行的返回结果进行处理,使其位于主线程

自定义Convert和CallAdapter

Fresco


Fresco是一个图片加载库,可以帮助我们加载图片显示,控制多线程,以及管理缓存和内存等

Fresco使用

  1. 引入依赖
dependencies {
  // 其他依赖
  compile 'com.facebook.fresco:fresco:0.12.0'
   // 在 API < 14 上的机器支持 WebP 时,需要添加
  compile 'com.facebook.fresco:animated-base-support:0.12.0'

  // 支持 GIF 动图,需要添加
  compile 'com.facebook.fresco:animated-gif:0.12.0'

  // 支持 WebP (静态图+动图),需要添加
  compile 'com.facebook.fresco:animated-webp:0.12.0'
  compile 'com.facebook.fresco:webpsupport:0.12.0'

  // 仅支持 WebP 静态图,需要添加
  compile 'com.facebook.fresco:webpsupport:0.12.0'
}

复制代码
  1. 初始化
Fresco.initialize(Context context);
复制代码
  1. 使用SimpleView
<com.facebook.drawee.view.SimpleDraweeView
    android:id="@+id/my_image_view"
    android:layout_width="130dp"
    android:layout_height="130dp"
    fresco:placeholderImage="@drawable/my_drawable"
  />
复制代码
  1. 加载图片
Uri uri = Uri.parse("https://raw.githubusercontent.com/facebook/fresco/gh-pages/static/logo.png");
SimpleDraweeView draweeView = (SimpleDraweeView) findViewById(R.id.my_image_view);
draweeView.setImageURI(uri);

复制代码
  1. 以上是Fresco的基本加载流程,此外,我们可以定制加载和显示的各个环节

Fresco由两部分组成,Drawees负责图片的呈现,ImagePipeline负责图片的下载解码和内存管理

Drawees

Drawees 负责图片的呈现。它由三个元素组成,有点像MVC模式。

DraweeView

  • 继承于 View, 负责图片的显示。
  • 一般情况下,使用 SimpleDraweeView 即可。 你可以在 XML 或者在 Java 代码中使用它,通过 setImageUri 给它设置一个 URI 来使用,这里有简单的入门教学:开始使用
  • 你可以使用 XML属性来达到各式各样的效果。

DraweeHierarchy

  • DraweeHierarchy 用于组织和维护最终绘制和呈现的 Drawable 对象,相当于MVC中的M。
  • 你可以通过它来在Java代码中自定义图片的展示

DraweeController

  • DraweeController 负责和 image loader 交互( Fresco 中默认为 image pipeline, 当然你也可以指定别的),可以创建一个这个类的实例,来实现对所要显示的图片做更多的控制。
  • 如果你还需要对Uri加载到的图片做一些额外的处理,那么你会需要这个类的。

DraweeControllerBuilder

  • DraweeControllers 由 DraweeControllerBuilder 采用 Builder 模式创建,创建之后,不可修改。具体参见: 使用ControllerBuilder。

Listeners

  • 使用 ControllerListener 的一个场景就是设置一个 Listener监听图片的下载。

ImagePipeline

  • Fresco 的 Image Pipeline 负责图片的获取和管理。图片可以来自远程服务器,本地文件,或者Content Provider,本地资源。压缩后的文件缓存在本地存储中,Bitmap数据缓存在内存中。
  • 在5.0系统以下,Image Pipeline 使用 pinned purgeables 将Bitmap数据避开Java堆内存,存在ashmem中。这要求图片不使用时,要显式地释放内存
  • SimpleDraweeView自动处理了这个释放过程,所以没有特殊情况,尽量使用SimpleDraweeView,在特殊的场合,如果有需要,也可以直接控制Image Pipeline。
  • ImagePipeline加载图片流程
  1. 检查内存缓存,如有,返回
  1. 后台线程开始后续工作
  2. 检查是否在未解码内存缓存中。如有,解码,变换,返回,然后缓存到内存缓存中。
  3. 检查是否在磁盘缓存中,如果有,变换,返回。缓存到未解码缓存和内存缓存中。
  4. 从网络或者本地加载。加载完成后,解码,变换,返回。存到各个缓存中。

![img](data:image/svg+xml;utf8,)

ImagePipeline的线程池

Image pipeline 默认有3个线程池:

  1. 3个线程用于网络下载
  1. 2个线程用于磁盘操作: 本地文件的读取,磁盘缓存操作。
  2. 2个线程用于CPU相关的操作: 解码,转换,以及后处理等后台操作。

ImagePipeline的 缓存

ImagePipeLine有三级缓存

  1. 解码后的Bitmap缓存
  2. 未解码图片的内存缓存
  3. 磁盘缓存

对比

功能

Fresco 相对于Glide/Picaso等拥有更多的功能,如图片的渐进式加载/动图/圆角等,

性能

Fresco采用三级缓存:

  1. 解码后的Bitmap缓存
  2. 未解码图片的内存缓存
  3. 磁盘缓存

Glide两级缓存:

  1. 根据ImageView控件尺寸获得对应的大小的bitmap来展示,可以缓存原始数据或者resize后数据
  2. 磁盘缓存

使用

Fresco通过CloseableReference管理图片,通过图片控件DraweeView来显示图片和控制图片释放,虽然扩展性高,但是扩展起来麻烦;对项目有一定侵入性

EventBus


EventBus使用了观察者模式,方便我们项目中进行数据传递和通信

使用

  1. 添加依赖
compile 'org.greenrobot:eventbus:3.0.0'
复制代码
  1. 注册和解绑
EventBus.getDefault().register(this);

EventBus.getDefault().unregister(this);
复制代码
  1. 添加订阅消息方法
@Subscribe(threadMode = ThreadMode.MAIN) 
public void onEvent(MessageEvent event) {
    /* Do something */
}
复制代码
  1. 发送消息
EventBus.getDefault().post(new MessageEvent("Hello !....."));
    
复制代码

@Subscribe注解

该注解内部有三个成员,分别是threadMode、sticky、priority。

  1. threadMode代表订阅方法所运行的线程
  2. sticky代表是否是粘性事件
  3. priority代表优先级

threadMode

  1. POSTING:表示订阅方法运行在发送事件的线程。
  2. MAIN:表示订阅方法运行在UI线程,由于UI线程不能阻塞,因此当使用MAIN的时候,订阅方法不应该耗时过长。
  3. BACKGROUND:表示订阅方法运行在后台线程,如果发送的事件线程不是UI线程,那么就使用该线程;如果发送事件的线程是UI线程,那么新建一个后台线程来调用订阅方法。
  4. ASYNC:订阅方法与发送事件始终不在同一个线程,即订阅方法始终会使用新的线程来运行。

sticky 粘性事件

在注册之前便把事件发生出去,等到注册之后便会收到最近发送的粘性事件(必须匹配)。注意:只会接收到最近发送的一次粘性事件,之前的会接受不到,demo

源码解析

参见链接

性能

  1. EventBus通过反射的方式对@Subscribe方法进行解析。
  2. 默认情况下,解析是运行时进行的,但是我们也可以通过设置和加载依赖库,使其编译时形成索引,其性能会大大提升

文章作者: ECM
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 ECM !
评论
  目录