Android 面试题 2018

算法

  1. 冒泡排序,两两排序,从后向前,大的向后,小的向前,时间复杂度O(n2)
  2.  
  3.  
  4. 优化

  1. 选择排序 :无序数组,第一次遍历n-1个数,找到最小的数值与第一个元素交换;
    第二次遍历n-2个数,找到最小的数值与第二个元素交换;
    。。。
    第n-1次遍历,找到最小的数值与第n-1个元素交换,排序完成。O(n2)
  2. java代码实现

  1. 插入排序 O(n2)

  1.   Java code

  1. 快速排序:

从数列中取出一个数作为key值;将比该数小的数置其左,大于或等于它的数置其右;对左右两个小数列重复第二步,直至各区间只有1个数 平均时间复杂度:O(N*logN)

  1.  Java code

Android

 

  1. 启动方式:点击图标的引发app中的默认Activity的启动。这种启动方式的特点是会启动一个新的进程来加载相应Activity
  2.  四大组件 Activity Broadcast ContentProvider Service
  3.  

  1. android任务栈称为Task,栈结构,后进先出,用于存放我们的Activity组件。
  2.  一个Task中的Actvity可以来自不同的App,同个AppActivity也可能不在一个Task中。
  3.  android启动模式standardsingleTopsingTasksingleInstance
  4.  standard每启动一个Activity都会重新创建新实例加入任务栈中
  5.  singleTop栈顶复用模式,若新Activity存在于栈顶,不重新创建实例,复用该Activity。Activity的onNewIntent方法被调用,此时ActivityonCreateonStart方法不会被调用,因为Activity并没有被重建。
  6. singleTop模式适用于接收到消息后显示的界面,如qq消息 新闻推送。
  7. singleTask栈内复用模式。单例模式,栈中是否存在当前Activity实例,存在将该实例置于栈顶,并将该Activity以上的Activity都从任务栈中移除,会回调onNewIntent方法。
  8. singleTask 模式适用的主界面activity(频繁使用主架构,如新闻,侧滑,应用主界面等有好多fragment,一般不会被销毁,可跳转其它的activity 界面再回主架构界面,此时其他Activity就销毁了。
  9. singleInstance模式,Activity在整个android系统内存中有且只有一个实例,该实例独享一个Task。换句话说,A应用需要启动的MainActivity 是singleInstance模式,当A启动后,系统会为它创建一个新的任务栈,然后A单独在这个新的任务栈中,如果此时B应用也要激活MainActivity,由于栈内复用的特性,则不会重新创建,而是两个应用共享一个Activity的实例
  10. 通过AndroidMenifest.xml文件为Activity指定启动模式

  1.  Intent中设置标志位(addFlags)Activity指定启动模式

  1.   URL Schema :页面内跳转协议,定义自己的scheme协议,可跳转app中的各个页面;通过scheme协议,服务器可以定制化告诉App跳转那个页面,可以通过通知栏消息定制化跳转页面,可以通过H5页面跳转页面等。
  2.  App可向操作系统注册一个 URL scheme,用于从其他应用中启动本应用。通过指定的 URL 字段,可以让应用在被调起后直接打开某些特定页面,如商品详情页等。也可执行某些指定动作,如完成支付等。也可在应用内通过 html 页来直接调用显示 app 内的某个页面
  3.  URL Schema使用场景:

 •服务器下发跳转路径,客户端根据服务器下发路径跳转相应页面
 •H5页面点击锚点,根据锚点具体路径跳转具体的页面
 •APP端收到服务器端下发的PUSH通知栏消息,根据消息的点击跳转路径跳转相关页面
 •APP根据URL跳转到另外一个APP指定页面

  1.  URL Schema协议格式:

xl://goods:8888/goodsDetail?goodsId=10011002 
上面路径中 SchemaHostportpathquery全部包含,平时使用路径就是这样。

 •xl代表该Schema 协议名称
 •goods代表Schema作用于哪个地址域
 •goodsDetail代表Schema指定的页面
 •goodsId代表传递的参数
 •8888代表该路径的端口号 

  1. URL Schema使用

AndroidManifest.xml中对<activity />标签增加<intent-filter />设置Schema

  1. 获取Schema跳转的参数 

  1. 调用方式

网页上:

原生调用

  1.  判断一个Schema是否有效 

   

  1. Binder是Android系统中进程间通讯(IPC)的一种方式,。Android中的四大组件Activity,Service,Broadcast,ContentProvider,不同的App等都运行在不同的进程中。
  2. Binder 架构
  1. Binder 采用 C/S 架构,从组件视角来说,包含 Client、 Server、 ServiceManager 以及 Binder 驱动,其中 ServiceManager 管理系统中各种服务。
  2. Binder 在 framework 层进行了封装,通过 JNI 技术调用 Native(C/C++)层的 Binder 架构。
  3. Binder 在 Native 层以 ioctl 的方式与 Binder 驱动通讯。

 

  1. Binder先注册服务端,客户端才有通讯目标,服务端通过 ServiceManager 注册服务,注册过程就是向 Binder 驱动的全局链表 binder_procs 中插入服务端的信息(binder_proc 结构体,每个 binder_proc 结构体中都有 todo 任务队列),然后向 ServiceManager 的 svcinfo 列表中缓存注册的服务。
  2. Binder获取服务端方式是通过 ServiceManager 向 svcinfo 列表中查询返回服务端的代理,svcinfo 列表是所有已注册服务的通讯录,保存了所有注册的服务信息。
  3. 向服务端发送请求,通过 BinderProxy 将请求参数发送给 ServiceManager,通过共享内存的方式使用内核方法 copy_from_user() 将我们的参数先拷贝到内核空间,这时我们的客户端进入等待状态,然后 Binder 驱动向服务端的 todo 队列插入一条事务,执行完后结果通过 copy_to_user() 将内核的结果拷贝到用户空间(这里只是执行了拷贝命令,并没有拷贝数据,binder只进行一次拷贝),唤醒等待的客户端并把结果响应回来,这样就完成了一次通讯。
  4. Kernel space 是 Linux 内核的运行空间,User space 是用户程序运行空间。 为了安全,它们是隔离的,即使用户的程序崩溃了,内核也不受影响。
  5. Kernel space 可执行任意命令,调用系统一切资源; User space 只能执行简单运算,不能直接调用系统资源,必须通过系统接口(又称 system call),才能向内核发出指令。
  6. 用户空间访问内核空间的唯一方式是系统调用;通过这个统一入口接口,所有的资源访问都在内核控制下执行,以免用户程序对系统资源的越权访问,从而保障系统安全、稳定
  7. 当进程执行系统调用而陷入内核代码中执行时,称进程处于内核运行态(简称内核态)此时处理器处于特权级最高的(0级)内核代码中执行。进程在执行用户自己的代码时,则称其处于用户运行态(用户态)。即此时处理器在特权级最低的(3级)用户代码中运行。处理器在特权等级高的时才能执行特权CPU指令。
  8. 一个用户空间想与另外一个用户空间进行通信怎么办呢?很自然想到的是让操作系统内核添加支持;传统的 Linux 通信机制,比如 Socket,管道等是内核支持的; Binder 不是 Linux 内核的一部分,它是怎么做到访问内核空间的呢? Linux 的动态可加载内核模块(Loadable Kernel Module,LKM)机制解决了这个问题;模块是有独立功能的程序,它可被单独编译,但不能独立运行。在运行时被链接到内核作为内核的一部分在内核空间运行。这样,Android系统可通过添加一个内核模块运行在内核空间,用户进程之间的通过这个模块作为桥梁,就可以完成通信了。这个运行在内核空间的,负责各个用户进程通过 Binder 通信的内核模块叫做 Binder 驱动;
  9. 驱动程序一般指设备驱动程序(Device Driver),一种可使计算机和设备通信的特殊程序。相当于硬件的接口,操作系统只有通过这个接口,才能控制硬件设备的工作;
  10. 用户空间中 binder_open(), binder_mmap(), binder_ioctl() 这些方法通过 system call 来调用内核空间 Binder 驱动中的方法。内核空间与用户空间共享内存通过 copy_from_user(), copy_to_user() 内核方法来完成用户空间与内核空间内存的数据传输。 Binder驱动中有一个全局的 binder_procs 链表保存了服务端的进程信息。
  11. Binder驱动,通过 binder_procs 链表记录所有创建的 binder_proc 结构体,每个 binder_proc 结构体都与用户空间用于 binder 通信的进程一一对应,每个进程有且只有一个 ProcessState 对象,通过单例模式保证。每个进程可以有很多个线程,每个线程对应一个 IPCThreadState 对象,IPCThreadState 对象也是单例模式, Binder 驱动层也有与之相对应结构,是 Binder_thread 结构体。在 binder_proc 结构体中通过成员变量 rb_root threads,来记录当前进程内所有的 binder_thread。
  12. Binder 线程池:每个 Server 进程启动时创建一个 binder 线程池,并向其注册一个 Binder 线程;之后 Server 进程也可向线程池注册新线程,或 Binder 驱动探测到无空闲 binder 线程时主动向 Server 进程注册新的的 binder 线程。对一个 Server 进程有一个最大 Binder 线程数限制,默认为16个 binder 线程,对所有 Client 端进程的 binder 请求都是交由 Server 端进程的 binder 线程来处理的。
  13. ServiceManager 提供了查询服务和注册服务功能。
  14. ServiceManager 启动在开机时,init 进程解析 init.rc 文件调用 service_manager.c 中的 main() 方法入口启动的。 native 层有一个 binder.c 封装了与 Binder 驱动交互方法。
  15. ServiceManager 启动分三步,首先打开驱动创建全局链表 binder_procs,然后将自己当前进程信息保存到 binder_procs 链表,最后开启 loop 不断的处理共享内存中的数据,并处理 BR_xxx 命令(ioctl 的命令,BR 可以理解为 binder reply 驱动处理完的响应)。
  16. 通过 ServiceManager 的 addService() 方法来注册服务
  17. ServiceManager 向 Binder 驱动发送 BC_TRANSACTION 命令(ioctl 的命令,BC 可以理解为 binder client 客户端发过来的请求命令)携带 ADD_SERVICE_TRANSACTION 命令,同时注册服务的线程进入等待状态 waitForResponse()。 Binder 驱动收到请求命令向 ServiceManager 的 todo 队列里面添加一条注册服务的事务。事务的任务就是创建服务端进程 binder_node 信息并插入到 binder_procs 链表中。
  18. 事务处理之后发送 BR_TRANSACTION 命令,ServiceManager 收到命令后向 svcinfo 列表中添加已经注册服务。最后发送 BR_REPLY 命令唤醒等待线程,通知注册成功。
  19. 用 Binder 时基本都是调用 framework 层封装的方法,AIDL 就是 framework 层提供的傻瓜式是使用方式。
  20. 通过 ServiceManager 获取到服务端的 BinderProxy 代理对象,通过调用 BinderProxy 将参数,方法标识(例如:TRANSACTION_test,AIDL中自动生成)传给 ServiceManager,同时客户端线程进入等待状态。
  21. ServiceManager 将用户空间的参数等请求数据复制到内核空间,并向服务端插入一条执行执行方法事务。事务执行完通知 ServiceManager 将执行结果从内核空间复制到用户空间,并唤醒等待的线程,响应结果,通讯结束。

 

  1. Android 广播分为:广播发送者、广播接收者 ,一个全局监听器,属于Android四大组件之一
  2.   BroadCastReiceiver应用场景
  •  Android不同组件间通信(含 :应用内 / 不同应用之间)
  • 多线程通信
  •  Android 系统在特定情况下的通信(电话呼入时、网络可用时)
  1.  Android将广播的发送者 和 接收者 解耦,使系统方便集成,更易扩展
  2.  
  3.  自定义广播接收者BroadcastReceiver
  1. 继承BroadcastReceivre基类
  1. 复写抽象方法onReceive()方法
  1.  
  1. 广播接收器接收到广播后,自动回调 onReceive()
  2. onReceive涉及与其他组件之间的交互,如发送Notification、启动Service等
  3. 默认情况下,广播接收器运行在 UI 线程,onReceive()方法不能执行耗时操作,否则导致ANR
  1. 广播接收器注册方式:静态注册、动态注册
  2.  在AndroidManifest.xml通过<receive>标签声明

  1.  动态注册:调用Context.registerReceiver()

 

  1. 动态广播,注册就得注销,否则会导致内存泄露,不允许重复注册、重复注销
  2.  

  1. 定义广播本质 = 定义广播的“意图(Intent)”

广播发送 = 广播发送者 将此广播的“意图(Intent)”通过sendBroadcast()发送

  1. 广播类型

普通广播(Normal Broadcast

系统广播(System Broadcast

有序广播(Ordered Broadcast

粘性广播(Sticky Broadcast

App应用内广播(Local Broadcast

  1. 普通广播

  1. 系统广播

Android中内置系统广播:涉及手机基本操作(如开机、网络状态变化等),发出相应广播

每个广播都有特定的Intent - Filter(包括具体的action),当用系统广播,只要在注册广播接收者时定义相关的action即可,不需手动发送广播。

  1.  有序广播,发送的广播被广播接收者按照先后顺序按照Priority属性值从大-小排序,Priority属性相同者,动态注册的广播优先
  2.  特点
  1. 接收广播按顺序
  2. 先接收的广播接收者可对广播截断,后广播接收者不再接收到广播;
  3. 先接收的广播接收者可对广播修改,后广播接收者将接收被修改后的广播
  1.  有序广播过程与普通广播类似,差异在于广播的发送方式:sendOrderedBroadcast(intent);
  2. 应用内广播可理解为局部广播,广播的发送者和接收者都同属于一个App。

相比于全局广播(普通广播),App应用内广播优势体现:安全性高 & 效率高

  1.  将全局广播设置成局部广播
  1. 静态注册广播时将exported属性设置为false,使得非本App内部发出的广播不被接收;
  2. 在广播发送和接收时,增设相应权限permission,用于权限验证;
  3. 发送广播时指定广播接收器包名,广播将只会发送到此包中的App内与之相匹配的有效广播接收器中。通过intent.setPackage(packageName)指定报名
  1. 用LocalBroadcastManager类实现应用内广播
  2. 不同注册方式广播接收器回调OnReceive(Context context,Intent intent)中的context返回值是不一样的:
  1. 静态注册(全局+应用内广播),回调onReceive(context, intent)中的context返回值:ReceiverRestrictedContext;
  2. 全局广播动态注册,回调onReceive(context, intent)中的context返回值:Activity Context;
  3. 应用内广播动态注册(LocalBroadcastManager方式),回调onReceive(context, intent)中的context返回值:Application Context。
  4. 应用内广播动态注册(非LocalBroadcastManager方式),回调onReceive(context, intent)中的context返回值:Activity Context;
  1. 当用户点击了屏幕,首先Activity先监测到,事件先传递到Activity中,Activity通过它的dispatchTouchEvent将事件分发到phoneWindow,phonewindow则会调用superdispatchTouchEvent方法的内部是调用了其内部类DecorView的superdispatchTouchEvent,而DecorView又会调用dispatchTouchEvent去进行事件分发,如果不拦截事件,那么就会继续下传到rootview,rootview中的操作是一样的,同样在dispatchTouchEvent内部调用onInterceptTouchEvent去判断是否拦截,不拦截就会把事件分发给下一个viewgroupA,拦截就直接在onTouchEvent返回true,viewgroupA中做的判断也是一样,最后事件传递到view1,view1是最底层控件,不会有onInterceptTouchEvent,它的选择就只有处理后不处理,处理就在onTouchEvent进行处理并返回true,不处理的话事件也不会被销毁,view1这时会把事件回传,经过上述流程后回传给activity,如果Activity还不处理,那么这个事件才会被销毁;
  2. Handler是可以通过发送和处理Message和Runnable对象来关联相应线程的MessageQueue。

子线程与主线程之间的通信就是靠Handler来完成。

  1. UI线程创建Handler实例,重写handleMessage,在handleMessage值实现UI线程逻辑,在子线程中需要更新UI时,新建一个Message对象,将消息的数据记录在这个消息对象Message的内部, Handler实例对象调用sendMessge方法把这个Message对象发送,之后这个消息会被存放于MessageQueue中等待被处理,此时MessageQueue的管家Looper正把MessageQueue存在的消息取出,通过回调dispatchMessage方法将消息传递给Handler的handleMessage方法,最终前面提到的消息会被Looper从MessageQueue中取出来传递给handleMessage方法,最终得到处理。这就是Handler机制整个的工作流程
  2. Handler引起的内存泄漏以及解决方法

原因:非静态内部类持有外部类的匿名引用,导致外部activity无法得到释放。

解决方法:handler内部持有外部的弱引用,并把handler改为静态内部类,在activity的onDestory()中调用handler的removeCallback()方法。

  1.  View绘制大致可以分为三个流程,分别是measure(测量),layout(布局),draw(绘制),这三者的顺序就是measure(测量)->layout(布局)->draw(绘制)。

measure: 判断是否需要重新计算View的大小,需要的话则计算;

layout: 判断是否需要重新计算View的位置,需要的话则计算;

draw: 判断是否需要重新绘制View,需要的话则重绘制

  1. 父View传递自身的MeasureSpec给子View,子View再根据自身设置的宽高参数LayoutParams属性和父View的MeasureSpec再去计算出子View自身的MeasureSpec,从而决定子View最终的宽和高。具体的计算的逻辑请查看getChildMeasureSpec()方法。
  2.  
  3. 版权声明:本文为博主原创文章,转载请附上博文链接!Android lint检查是一个静态代码分析工具,它能够对你的Android项目中潜在的bug,可优化的代码,安全性,性能,可用性,可访问性,国际性等进行检查。

  1. Kotlin当中所有的类型都分为“可空类型”和“非空类型”,比如:”String”和”String?”,String就是非空类型,String?就是可空类型,非空类型是不允许被null赋值的,否则,代码会编译报错,而可空类型就是可以允许被null赋值
  2. Kotlin 可推断类型 类型转换是使用as关键字
  3. Kotlin when =java中 else if和switch的结合体
  4. Kotlin当中的构造器的名字可不是跟Java一样,和类名相同,其构造器的名字必须是:constructor
  5. 具名参数,变长参数,默认参数
  6. WebSettings 对WebView配置
  1. setJavaScriptEnabled(boolean flag):是否支持 Js 使用
  2. setCacheMode(int mode):设置 WebView 的缓存模式。
  3. setAppCacheEnabled(boolean flag):是否启用缓存模式
  4. setLoadsImagesAutomatically(boolean flag):支持自动加载图片
  5. setAllowFileAccessFromFileURLs(boolean flag::允许通过 file url 加载的 Javascript 读取其他的本地文件
  1. WebViewClient 对WebView进行处理各种通知和请求事件
  1. onLoadResource(WebView view, String url):WebView 加载页面资源时会回调,每一个资源产生的一次网络加载,除非本地有当前 url 对应有缓存,否则就会加载
  2. shouldInterceptRequest(WebView view, String url):WebView 可以拦截某一次的 request 来返回我们自己加载的数据,这个方法在后面缓存会有很大作用。
  3. shouldOverrideUrlLoading(WebView view, String url):是否在 WebView 内加载页面

 

 

  1. WebChromeClient 辅助 WebView 处理 Javascript 的对话框,网站图标,网站标题等等
  1. onConsoleMessage(String message, int lineNumber,String sourceID):输出 Web 端日志。
  2. onProgressChanged(WebView view, int newProgress):当前 WebView 加载网页进度。
  3. onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result):处理 JS 中的 Prompt对话框
  4. onJsAlert(WebView view, String url, String message, JsResult result): Js 中调用 alert() 函数,产生的对话框。
  5. onReceivedTitle(WebView view, String title):接收web页面的 Title。
  6. onReceivedIcon(WebView view, Bitmap icon):接收web页面的icon。
  1. JSBridge

1、使用系统方法 addJavascriptInterface 注入 java 对象来实现。

2、利用 WebViewClient 中 shouldOverrideUrlLoading (WebView view, String url) 接口,拦截操作。这个就是很多公司在用的 scheme 方式,通过制定url协议,双方各自解析,使用iframe来调用native代码,实现互通。

3、利用 WebChromeClient 中的 onJsAlert、onJsConfirm、onJsPrompt 提示接口,同样也是拦截操作

  1. 虽然可以通过注入方式来实现 WebView 和 JS 交互,但是实现功能的同时也带了安全问题,通过注入的 Java 类作为桥梁,JS 就可以利用这个漏洞。
  2. Android WebView自带的缓存机制其实就是 H5页面的缓存机制
  3. 组件化目标
  1. 降低app与组件的依赖关系,缺少任何一个组件app都是可以存在并正常运行的
  2. 组件可以单独运行。
  1. 组件化和插件化的最大区别(应该也是唯一区别)就是组件化在运行时不具备动态添加和修改组件的功能,但是插件化是可以的
  2. 插件化使我们具备极大灵活性。但是苦于目前还没有一个完全合适、完美兼容的插件化方案,特别是对于已经有几十万代码量的一个成熟产品来讲,套用任何一个插件化方案都是很危险的工作。所以我们决定先从组件化做起,本着做一个最彻底的组件化方案的思路去进行代码的重构
  3. 解耦:一种是基础库library,这些代码被其他组件直接引用。比如网络库module可以认为是一个library

另一种我们称之为Component,这种module是一个完整的功能模块。比如读书或者分享module就是一个Component。

  1. 组件的单独调试,要把apply plugin: 'com.android.library'切换成apply plugin: 'com.android.application'就可以,但是我们还需要修改一下AndroidManifest文件,因为一个单独调试需要有一个入口的actiivity。
  2. 设置一个变量isRunAlone,标记当前是否需要单独调试,根据isRunAlone的取值,使用不同的gradle插件和AndroidManifest文件,甚至可以添加Application等Java文件,以便可以做一下初始化的操作。为了避免不同组件之间资源名重复,在每个组件的build.gradle中增加resourcePrefix "xxx_",从而固定每个组件的资源前缀。读书组件的build.gradle的示例

  1. 组件数据传输

主项目和组件、组件与组件之间不能直接使用类的相互引用来进行数据交互。那么如何做到这个隔离呢?在这里我们采用接口+实现的结构。每个组件声明自己提供的服务Service,这些Service都是一些抽象类或者接口,组件负责将这些Service实现并注册到一个统一的路由Router中去。如果要使用某个组件的功能,只需要向Router请求这个Service的实现。与Binder的C/S架构很相像。

  1. 组件间UI跳转

路由跳转需要支持传递基本类型和自定义类型(例如Object)
路由的跳转需要和组件的生命周期一致,即只有加载的组件才可以跳转,卸载后的组件是不可达的
最好生成路由表,组件对外提供的路由可以轻松查阅到

  1. 组件间UI跳转必要的配置

组件化框架中共用的依赖库增加arouter

跳转目标组件build.gradle中,增加以下配置:

  1. 传递自定义类型

组件间在编译期代码和资源都是完全隔离的,所以自定义类必须定义在share组件向外提供的服务中。所以我们在component中,定义Author类:为了能在路由中传递,按照ARouter的要求,还需要自己实现SerializationService:

  1. 控制生命周期

跳转的逻辑已经与组件化生命周期绑定在一起。
这里就用到ARouter自带的拦截器功能,每个组件都需要定义一个拦截器,当组件卸载之后需要拦截住该组件的跳转入口。

  1. Android的Dalvik/ART虚拟机类似JAVA的JVM虚拟机,在运行程序时首先需将对应的类加载到内存中。因此代码可动态加载可执行文件。Android可实现动态加载插件。
  2. 动态加载的基础是ClassLoader,专门用来处理类加载工作,也叫类加载器,一个APP 有多个类加载器
  3. Android系启动时创建一个Boot类的ClassLoader实例,加载系统Framework层级需要的类,应用里也要用到系统类,APP启动时会把这个Boot类型的ClassLoader传进来。APP也会创建一个ClassLoader实例,用于加载自己dex文件中的类。一个运行的Android应用至少有2个ClassLoader
  4. 创建一个ClassLoader实例,需用一个现有ClassLoader实例作为新建的实例Parent。这样Android系统里所有ClassLoader实例都被关联,这是ClassLoader 双亲代理模型(Parent-Delegation Model)。
  5. loadClass方法在加载类的实例的时会先查当前ClassLoader实例是否加载过此类,有就返回;没有。查Parent是否已经加载过此类,若有,返回Parent加载的类;如果继承路线上的ClassLoader都没有加载,才由Child执行类的加载工作;
  6. 若一个类被位于树根的ClassLoader加载过,那么在以后整个系统的生命周期内,该类不被重新加载。
  7. 同一个Class = 相同的 ClassName + PackageName + ClassLoader
  8. ClassLoader双亲代理的实现大部分在loadClass方法里,可重写loadClass()避开双亲代理的框架,这样可重新加载已加载过的类,也可在加载类的时候注入一些代码。这是一种Hack的开发方式,采用这种开发方式的程序稳定性可能比较差,可以实现一些“黑科技”功能。
  9. Android动态加载问题:

动态加载了一个新的组件类进来,没有注册的话还是无法工作;

运行时动态加载进来的类,类的资源ID根本和现有的Resource实例中资源ID对不上;

  1. PathClassLoader只能加载系统/data/data/包名目录下的apk;

DexClassLoader可以加载jar/apk/dex,可以从SD卡中加载的apk;

  1. 使用DexClassLoader代替PathClassLoader除了可以解决Dex加载与系统版本密切问题之外,还可以将第三方apk复制到外置SD卡上减少应用安装后的体积
  2. 宿主获得插件apk的资源文件,android.content.res.AssetManager.java中包含一个私有方法addAssetPath。只需要将apk的路径作为参数传入,就可以获得对应的AssetsManager对象,从而创建一个Resources对象,然后就可以从Resource对象中访问apk中的资源了。
  3. 宿主需要用到相关资源的时候需跟插件约定好对应名称,以防出现找不到的情况
  4. 跳转插件的Activity方法常见的方法有宿主代理Activity模式和宿主动态创建Activity模式。
  5. 宿主代理无需在宿主中注册Activity,所有跳转均由一个傀儡Activity完成,无需过多的改变宿主即可完成插件开发,但插件Activity不享有系统的生命周期,其生命周期由宿主通过反射的方式传递。
  6. 动态创建,Activity有着自己的生命周期,但是必须提前在宿主AndroidManifest文件中注册。
  7. 插件Activity没在主项目的Manifest里面注册,无法经历系统Framework层级的初始化,导致获得的Activity实例并没有生命周期和无法使用res资源。
  8. 处理插件Activity的生命周期(代理Activity模式)
  1. 在ProxyActivity生命周期里用反射调用插件Activity相应生命周期的方法,简单粗暴。
  2. 把插件Activity的生命周期抽象成接口,在ProxyActivity的生命周期里调用。另外,多了这一层接口,也方便主项目控制插件Activity。
  1. 要启动插件某个Activity(如PlugActivity)时,动态创建一个TargetActivity,TargetActivity继承PlugActivity的所有共有行为,而这个TargetActivity的包名与注册的TargetActivity一致,就能以标准的方式启动这个Activity。
  2. 运行时动态创建类的工具有dexmakerasmdex,均能实现动态字节码操作,前者是创建dex文件,后者创建class文件。
  3. 运行时创建一个编译好并能运行的类叫做“动态字节码操作(runtime bytecode manipulation)”,使用dexmaker工具能创建一个dex文件。
  4. Android,虚拟机加载类的时,通过ClassLoader的loadClass方法,可重载loadClass方法并改写类的加载逻辑,在加载PlugActivity的时,把其换成TargetActivity。把启动插件里的PlugActivity变成启动动态创建的TargetActivity。
  5. 动态类创建,注册一个通用的Activity就能给多个Activity使用,也存在问题

用同个注册Activity,需要在Manifest注册的属性无法做到每个Activity都自定义配置;

插件的权限,无法动态注册,插件需要的权限都得在宿主中注册,无法动态添加权限;

插件的Activity无法开启独立进程,因为这需要在Manifest里面注册;

动态字节码操作涉及到Hack开发,所以相比代理模式起来不稳定

  1. JNI使用包含了动态加载,APP运行时动态加载.so库并通过JNI调用其封装好的方法。后者一般是使用NDK工具从C/C++代码编译而成,运行在Native层,效率比执行在虚拟机的Java代码高,所以Android通过动态加载.so库来完成一些对性能比较有需求的工作(比如T9搜索、或者Bitmap的解码、图片高斯模糊处理等)。此外,由于.so库是由C++编译而来的,只能被反编译成汇编代码,相比Smali更难被破解,因此.so库也可以被用于安全领域
  2. DexPathList对象中的dexElements列表是类加载的一个核心,一个类如果能被成功加载,那么它的dex一定 会出现在dexElements所对应的dex文件中,在dexElements前面出现dex会被先加载,一旦Class加载成功, 会立即返回,就是,一定要保证我们的hotpacth dex文件出现在dexElements列表前面。
  3. 实现热更新,就需要我们在运行时去更改PathClassLoader.pathList.dexElements,由于这些属性都是private,因此需要通过反射来修改。另外,构造我们自己的dex文件 所对应的dexElements数组,通过构造一个DexClassLoader对象来加载我们的dex文件,并且调用一次dexClassLoader.loadClass(dummyClassName),这样dexClassLoader.pathList.dexElements中,会包含我们的dex,通过 dexClassLoader.pathList.dexElements插入到系统默认的classLoader.pathList.dexElements列表前面,就可以让系统优先加载我们的dex中的类,从而可以实现热更新了。
  4. AndFix使用
  1. 先添加依赖
    compile 'com.alipay.euler:andfix:0.3.1@aar'
  2. 然后在Application.onCreate() 中添加以下代码

patchManager = new PatchManager(context);

patchManager.init(appversion);//current version

patchManager.loadPatch();

  1. 获取appversion
    String appversion= getPackageManager().getPackageInfo(getPackageName(), 0).versionName;每次appversion变更都会导致所有补丁被删除,如果appversion没有改变,则会加载已经保存的所有补丁。
  2. 然后在需要的地方调用PatchManager的addPatch方法加载新补丁,比如可以在下载补丁文件之后调用。
  3. 之后就是打补丁的过程了,首先生成一个apk文件,然后更改代码,在修复bug后生成另一个apk。
    通过官方提供的工具apkpatch生成一个.apatch格式的补丁文件,需要提供原apk,修复后的apk,以及一个签名文件。
  4. 通过网络传输或者adb push的方式将apatch文件传到手机上,然后运行到addPatch的时候就会加载补丁。
    加载过的补丁会被保存到data/packagename/files/apatch_opt目录下,所以下载过来的补丁用过一次就可以删除了。
  1. AndFix原理apkpatch将两个apk做对比,找出不同部分。生成的apatch文件,后缀改成zip再解压开,里面有一个dex文件。通过jadx查看一下源码,里面就是被修复的代码所在的类文件,这些更改过的类都加上了一个_CF的后缀,并且变动的方法都被加上了一个叫@MethodReplace的annotation,通过clazz和method指定了需要替换的方法。然后客户端sdk得到补丁文件后就会根据annotation寻找需要替换的方法。最后由JNI层完成方法的替换。
  2. 多次打补丁

如果本地保存了多个补丁,那么AndFix会按照补丁生成的时间顺序加载补丁。具体是根据.apatch文件中的PATCH.MF的字段Created-Time。

  1. AndFix安全性
  1. AndFix验证,如果补丁文件的证书和当前apk的证书不是同一个的话,就不能加载补丁
  2. 需要验证optimize file的指纹,应该是为了防止有人替换掉本地保存补丁文件,要验证MD5码,然而SecurityChecker类里面也已经做了这个工作。。但是这个MD5码是保存在sharedpreference里面,如果手机已经root那么还是可以被访问的。
  1. AndFix局限性
  1. 无法添加新类和新的字段
  2. 需要使用加固前的apk制作补丁,但是补丁文件很容易被反编译,也就是修改过的类源码容易泄露。
  3. 使用加固平台可能会使热补丁功能失效。

 

  1.  
  2.  
  3. 随着应用版本的不断迭代,App变得非常庞大而臃肿。新需求接踵而至,旧需求废弃无用而占着空间,工程变得混乱。虽然会采用不同的包名隔离开各个需求模块,但是往往还是觉得如果需求能像插件一样分模块,需要的时候插上去,废弃的时候拔下来就好了
  4. 同一个Class = 相同的 ClassName + PackageName + ClassLoader
  5. 是把一些辅助性的代码(如xxxUtils.java)写在B项目里生成B.apk,把B.apk放到主项目A的assets文件夹中,利用反射的原理让A项目可以调用B.apk中的xxxUtils.java。其实可以更简单的理解为B是一个Android library。这样直接用apk反射调用而不是用library的形式有一个好处就是规避项目编译过程中的dex超过65535方法数Bug,也有可能是为了日后支持例如资源共享,界面跳转之类的,因为传统Android library是不能向宿主获取资源的。
  6. 在app/src/main/目录下创建asserts文件夹,把编译好pluginA.apk放到里面。创建一个AssertsDexLoader.java用来动态加载pluginA工程。
  7. AssertsDexLoader逻辑为拷贝asserts里的apk文件到系统的/data/data/appPackageDir/里,再把ClassLoader,拷贝的目录及文件三个参数传递给installBundleDexs()即可动态加载其方法至内存。
  8. 在需要调用pluginA内方法的地方通过反射调用即可
  9.  ClassLoader顾名思义就是类的加载者,通俗的来说就是把class文件中的类加载到内存里。App启动的时候会创建一个自己的ClassLoader实例,将我们创建的ClassLoader挂载到App的ClassLoader下即可实现类的动态加载。
  10.  当插件apk的体积变庞大之后,这种临时加载的过程会变得漫长,不过好在动态加载可以当宿主需要的时候才加载到内存中。
  11. 反射调用也必须按照约定的命名规范去调用,否则容易导致类或方法无法找到。
  12. 而且Dex的加载与系统版本依赖严重,可能会导致新版SDK不支持等问题。
  13.  
  14.  
  15.  
  16.  
  17.  
  18.   
  19.  
  20.  
  21.  
  22.  
  23.  
  24.  

java

  1. 面向对象三大特性:封装、继承、多态。从一定角度来看,封装和继承几乎都是为多态而准备的。
  2. 多态的定义指允许不同类的对象对同一函数做出响应。即同一函数可以根据发送对象的不同而采用多种不同的行为方式。
  3. 多态存在三个必要条件 一、继承; 二、重写; 三、父类引用指向子类对象。
  4. 作用:

1.可替换性(substitutability)。多态对已存在代码具有可替换性。

2.可扩充性(extensibility)。多态对代码具有可扩充性。增加新的子类不影响已存在类的多态性、继承性,以及其他特性的运行和操作。

3.接口性(interface-ability)。多态是超类通过方法签名,向子类提供了一个共同接口,由子类来完善或者覆盖它而实现的。

4.灵活性(flexibility)。它在应用中体现了灵活多样操作,提高使用效率。

5.简化性(simplicity)。多态简化对应代码编写和修改,尤其在处理大量对象的运算和操作时。

  1. 常量池:Java代码被编译成class文件时,生成一个常量池(Constant pool)的数据结构,用以保存字面常量和符号引用(类名、方法名、接口名和字段名等
  2. 字符串可通过两种方式进行初始化:字面常量和String对象。
  3. String:字符串常量,字符串长度不可变。
  4. StringBuffer:字符串变量(线程安全, 主用在全局变量中)。要频繁对字符串内容进行修改,出于效率考虑最好使用StringBuffer,转成String类型,可用StringBuffer.toString()

StringBuilder:字符串变量(非线程安全)。在内部,StringBuilder对象被当作是一个包含字符序列的变长数组。

  1.  java中==和equals和hashCode的区别 

基本数据类型的==比较的值相等
类的==比较的内存的地址,即是否是同一个对象,在不覆盖equals的情况下,同比较内存地址,原实现也为 == ,如String等重写了equals方法.
hashCode
也是Object类的一个方法。返回一个离散的int型整数。在集合类操作中使用,为了提高查询速度。(HashMapHashSet等比较是否为同一个)

如果两个对象equalsJava运行时环境会认为他们的hashcode一定相等。

如果两个对象不equals,他们的hashcode有可能相等。

如果两个对象hashcode相等,他们不一定equals

如果两个对象hashcode不相等,他们一定不equals

  1.  TCP/IP协议栈互联网主流协议

 TCP/IP模型各个层次分别对应于不同的协议。TCP/IP协议栈是数据通信协议的集合 ,包含许多协议。其协议栈名字来源于其中最主要的两个协议TCP(传输控制协议)和IP(网际协议)。TCP/IP协议栈负责确保网络设备之间能够通信。它是一组规则,规定了信息如何在网络中传输

  1. 网络层:在网络之间转发数据包

1数据跨网络传递,则需要使用逻辑地址来寻址。

2路由:将数据报文从一个网络转发到另一个网络。

  1.  IP(Internet Protocol):IP为网络层主要协议,一是提供逻辑编址,二是提供路由功能,三是报文的封装和解封装。ICMP、ARP、RARP协议辅助IP工作。
  2.  传输层
    1. 分段上层数据;
    2. 建立端到端连接;
    3. 将数据从一端主机传送到另一端主机;
    4. 保证数据按序、可靠、正确传输。
  3.  传输层协议主要包含传输控制协议TCP(transfer control protocol)和用户数据报文协议UDP(user datagram protocol)
  4.  TCP 面向连接的、可靠的字节流服务。面向连接意味着使用TCP协议作为传输层协议的两个应用之间在相互交换数据之前必须建立一个TCP连接。TCP通过确认、校验、重组等机制为上层提供可靠传输。TCP带来大量的开销。
  5. UDP简单的、面向数据报服务。不可靠,不保证报文能到达目的地。UDP适用于关注传输效率的应用,如SNMP、Radius等,SNMP监控网络并断续发送告警等消息,UDP还适用于本身具备可靠性机制的应用层协议。
  6.  TCP连接的建立三次握手

  1.  TCP连接的终止则要四次握手

  1.  UDP提供面向无连接服务。传输数据之前源端和目的端不需要建立连接,服务器可同时向多个客户端传输相同的消息。用于对传输效率要求高。
  2.  IP地址: 为实现网络中不同设备之间的通信,设备唯一的标识
  3.  端口  区分一台主机的多个不同应用程序
  4.   IP地址+端口号组成Socket,Socket是网络上运行的程序之间双向通信链路的终结点,是TCP和UDP的基础
  5.   Socket原理机制:
  1. 通信的两端都有Socket
  2. 网络通信其实就是Socket间的通信
  3. 数据在两个Socket间通过IO传输

  1.  服务器端:

创建ServerSocket对象,绑定监听端口

 通过accept()方法监听客户端请求

 连接建立后,通过输入流读取客户端发送的请求信息

  通过输出流向客户端发送响应信息

关闭相关资源

  1. 客户端:
  •  创建Socket对象,指明需要连接的服务器的地址和端口号
  •  连接建立后,通过输出流想服务器端发送请求信息
  •  通过输入流获取服务器响应的信息
  •  关闭响应资源 
  1. 多线程实现服务器与多客户端之间的通信
  1. 服务器端创建ServerSocket,循环调用accept()等待客户端连接
  2. 客户端创建一个socket并请求和服务器端连接
  3. 服务器端接受客户端请求,创建socket与该客户建立专线连接
  4. 建立连接的两个socket在一个单独的线程上对话
  5. 服务器端继续等待新的连接
  1.  UDP编程 :先将传输的数据定义成数据报(Datagram),大小限制在64k,在数据报中指明数据索要达到的Socket(主机地址和端口号),再将数据报发出
  2.  UDP编程服务器端实现
  1. 创建DatagramSocket,指定端口号
  2. 创建DatagramPacket
  3. 接受客户端发送的数据信息
  4. 读取数据
  1.  UDP编程:客户端实现
  1. 定义发送信息,创建DatagramPacket,包含该发送的信息
  2. 创建DatagramSocket, 发送数据
  1.  网址通过http协议发出去,网址经过DNS域名解析,解析成指定的ip地址,并在80端口上监听用户的请求。服务器监听到请求之后,会以三种方式返回给客户端:HTML、XML、JASON
  2.   GET和POST差别:

1 GET请求在通过URL提交数据,POST请求,数据在HTML HEADER内提交。 
2 GET请求,服务器端用Request.QueryString获取变量的值,POST请求,服务器用Request.Form获取提交数据。 
3 GET请求提交数据不大于2KB(主要是URL长度限制),而POST无此限制

4如果用户输入的数据不是中文字符而且包含敏感数据,那么还是使用POST为好。

  1. HTTP返回数据三种方式:
  1. 以HTML、XML、JSON返回
  2. 在网络流量上考虑JSON要比XML方式要好一些,便于解析。
  3. Android中,一般使用xml和Json数据解析
  1.  并行:多个CPU实例同时执行一段逻辑

并发:通过CPU调度算法,让用户看上去同时执行,实际上从CPU操作层面上不是真正的同时。

  1.  进程:程序不能单独运行,只有将程序装载到内存中,系统为它分配资源才能运行,而这种执行的程序就称之为进程。程序和进程的区别就在于:程序是指令的集合,它是进程运行的静态描述文本;进程是程序的一次执行活动,属于动态概念。
  2.  操作系统的调度下,多个进程可以实现并发地执行。提高了CPU利用率。进程的出现让每个用户感觉到自己独享CPU。
  3.  因为并发,发明进程,又进一步发明线程。只不过进程和线程的并发层次不同:进程属于在处理器这一层上提供的抽象;线程则属于在进程这个层次上再提供了一层并发的抽象。如果我们进入计算机体系结构里,就会发现,流水线提供的也是一种并发,不过是指令级的并发。这样,流水线、线程、进程就从低到高在三个层次上提供并发!
  4.   线程是进程的一个实体, 是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。
  5.  进程和线程的主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序 健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程
  6.   同步:Java 中同步指的是通过人为的控制和调度,保证共享资源的多线程访问成为线程安全,保证结果准确。
  7. Java线程五中基本状态
  1. 新建状态(New):当线程对象对创建后,即进入了新建状态
  2. 就绪状态(Runnable):调用线程start(),线程进入就绪状态。处于就绪状态的线程,此线程随时等待CPU调度执行。
  3. 运行状态(Running):CPU调度处于就绪状态线程时,此时线程才得以真正执行,进入到运行状态。
  4. 阻塞状态(Blocked):处于运行状态的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入运行状态
  5. 阻塞状态分为三种:
  1. 等待阻塞:运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态;
  2. 同步阻塞 : 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;
  3. 其他阻塞 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
  1. 死亡状态(Dead):线程执行完了或者因异常退出了run()方法,该线程结束生命周期
  1.  
  2.  

 

 

结构数据

  1.  
  2.  
  3.  
  4.  

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值