Android 基础知识及概念



一、语言基础及应用

1.1 Java 核心思想

核心思想:面向对象,一切事物皆对象。

1.2 Java 语言的特点与是 OOP 思想

Java 语言有很多的优点,可靠、安全、编译和解释型语言、分布式、多线程、完全面向对象、与平台无关性等等。

  • 抽象:抽象就是忽略一个主题中与当前目标无关的那些方面,以便更充分地注意与当前目标有关的方面。抽象并不打算了解全部问题,而只是选择其中的一部分,暂时不用部分细节。抽象包括两个方面,一是过程抽象,二是数据抽象
  • 继承:继承是一种联结类的层次模型,并且允许和鼓励类的重用,它提供了一种明确表述共性的方法。对象的一个新类可以从现有的类中派生,这个过程称为类继承。新类继承了原始类的特性,新类称为原始类的派生类(子类),而原始类称为新类的基类(父类)。派生类可以从它的基类那里继承方法和实例变量,并且类可以修改或增加新的方法使之更适合特殊的需要
  • 封装:封装是把过程和数据包围起来,对数据的访问只能通过已定义的界面。面向对象计算始于这个基本概念,即现实世界可以被描绘成一系列完全自治、封装的对象,这些对象通过一个受保护的接口访问其他对象
  • 多态性:多态性是指允许不同类的对象对同一消息作出响应。多态性包括参数化多态性和包含多态性。多态性语言具有灵活、抽象、行为共享、代码共享的优势,很好的解决了应用程序函数同名问题

1.3 Java 注解,反射,泛型的理解与作用

1.3.1 注解

什么是注解

注解也叫元数据,例如我们常见的 @Override 和 @Deprecated,注解是 JDK 1.5 版本开始引入的一个特性,用于对代码进行说明,可以对包、类、接口、字段、方法参数、局部变量等进行注解。

常用的注解可以分为三类:

  • 一类是 Java 自带的标准注解,包括 @Override(标明重写某个方法)、@Deprecated(标明某个类或方法过时) 和 @SuppressWarnings(标明要忽略的警告),使用这些注解后编译器就会进行检查
  • 一类为元注解,元注解是用于定义注解的注解,包括 @Retention(标明注解被保留的阶段)、@Target(标明注解使用的范围)、@Inherited(标明注解可继承)、@Documented(标明是否生成 javadoc 文档)
  • 一类为自定义注解,可以根据自己的需求定义注解

注解的用途

在看注解的用途之前,有必要简单的介绍下XML和注解区别,

  • 注解:是一种分散式的元数据,与源代码紧绑定
  • xml:是一种集中式的元数据,与源代码无绑定

当然网上存在各种 XML 与注解的辩论哪个更好,这里不作评论和介绍,主要介绍一下注解的主要用途:

  • 生成文档,通过代码里标识的元数据生成 javadoc 文档
  • 编译检查,通过代码里标识的元数据让编译器在编译期间进行检查验证
  • 编译时动态处理,编译时通过代码里标识的元数据动态处理,例如动态生成代码
  • 运行时动态处理,运行时通过代码里标识的元数据动态处理,例如使用反射注入实例
1.3.2 反射

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

  • 在运行时判断任意一个对象所属的类
  • 在运行时构造任意一个类的对象
  • 在运行时判断任意一个类所具有的成员变量和方法
  • 在运行时调用任意一个对象的方法;生成动态代理
1.3.3 泛型

泛型,即“参数化类型”。一提到参数,最熟悉的就是定义方法时有形参,然后调用此方法时传递实参。那么参数化类型怎么理解呢?顾名思义,就是将类型由原来的具体的类型参数化,类似于方法中的变量参数,此时类型也定义成参数形式(可以称之为类型形参),然后在使用/调用时传入具体的类型(类型实参)。

泛型在使用中还有一些规则和限制:

  • 泛型的类型参数只能是类类型(包括自定义类),不能是简单类型
  • 同一种泛型可以对应多个版本(因为参数类型是不确定的),不同版本的泛型类实例是不兼容的
  • 泛型的类型参数可以有多个
  • 泛型的参数类型可以使用 extends 语句,例如,习惯上成为“有界类型”
  • 泛型的参数类型还可以是通配符类型

泛型的作用:

  • 限定类型就已经有很大作用了,特别是写基础架构的时候,不需要以前那样的检查,我们的代码量和开发速度都可以提升一大截
  • 能够进行编译期间类型检查
  • 限定类型啊 通俗点比喻 (箱子贴标签)这个箱子是放苹果的 那个箱子是放橘子的
  • 封装一些共性问题,可以简化很多代码,使代码更加有层次,简单
  • 比 object 类范围明显缩小了,提高了程序运行的效率

1.4 JVM 是如何回收对象的,有哪些方法等

Java 将程序员从内存管理中解放出来,使得我们在编写代码的时候不用手动的分配和释放内存,内存管理的任务由JVM承担起来。

对象存活的判定 :当一个对象不会再被使用的时候,我们会说这对象已经死亡。对象何时死亡,写程序的人应当是最清楚的。如果计算机也要弄清楚这件事情,就需要使用一些方法来进行对象存活判定,常见的方法有引用计数(Reference Counting)有可达性分析(Reachability Analysis)两种。

引用计数算法的大致思想是给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。它的实现简单,判定效率也很高,在大部分情况下它都是一个不错的算法,也有一些比较著名的应用案例,例如微软 COM(Component Object Model) 技术、使用 ActionScript 3 的FlashPlayer、Python 语言和在游戏脚本领域得到许多应用的 Squirrel 中都使用了引用计数算法进行内存管理。但是,至少 Java 语言里面没有选用引用计数算法来管理内存,其中最主要原因是它没有一个优雅的方案去对象之间相互循环引用的问题:当两个对象互相引用,即使它们都无法被外界使用时,它们的引用计数器也不会为 0。

垃圾对象被回收过程:在可达性分析算法中不可达的对象,并非被立即回收,而是要经过两次标记:

  • 对象是否覆盖 finalize() 方法,或者 finalize() 方法已经被虚拟机调用过,若未覆盖或者已调用则回收
  • 覆盖 finalize() 方法并且未被调用,则对象会放置在 F-Queue 队列中,并在稍后由一个虚拟机自动建立的、低优先级的 Finalizer 线程去触发 finalize 方法,但并不会等待它运行结束(原因是若 finalizer 执行缓慢或者死循环,则 F-Queue 队列其他对象处于永久等待,导致整个内存回收系统崩溃)。稍后 GC 将对 F-Queue 中的对象再次标记(只要重新与引用链上的任何一个对象建立关联,就被移除即将回收集合。否则进行回收)

1.5 LinkedList 与 ArrayList 的区别

ArrayList 和 LinkedList 的大致区别如下:

  • ArrayList 是实现了基于动态数组的数据结构,LinkedList 基于链表的数据结构
  • 对于随机访问 get 和 set,ArrayList 觉得优于 LinkedList,因为 LinkedList 要移动指针
  • 对于新增和删除操作 add 和 remove,LinedList 比较占优势,因为 ArrayList 要移动数据

1.6 HashTable与HashMap的区别

HashTable 和 HashMap 区别:

  • 都属于 Map 接口的类,实现了将惟一键映射到特定的值上
  • 继承的父类不同:Hashtable 继承自 Dictionary 类,而 HashMap 继承自 AbstractMap 类。但二者都实现了 Map 接口
  • 线程安全性不同:javadoc 中关于 hashmap 的一段描述如下:此实现不是同步的。如果多个线程同时访问一个哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须保持外部同步
  • HashMap 类没有分类或者排序。它允许一个 null 键和多个 null 值,Hashtable 不允许
  • Hashtable 类似于 HashMap,但是不允许 null 键和 null 值。它也比 HashMap 慢,因为它是同步的
  • HashMap 默认长度是 16,扩容是原先的 2 倍;Hashtable 默认长度是 11,扩容是原先的 2n+1

1.7 LruCache 底层原理

LruCache 使用一个 LinkedHashMap 简单的实现内存的缓存,没有软引用,都是强引用。

如果添加的数据大于设置的最大值,就删除最先缓存的数据来调整内存。maxSize 是通过构造方法初始化的值,他表示这个缓存能缓存的最大值是多少。

size 在添加和移除缓存都被更新值, 他通过 safeSizeOf 这个方法更新值。 safeSizeOf 默认返回 1,但一般我们会根据 maxSize 重写这个方法,比如认为 maxSize 代表是 KB 的话,那么就以 KB 为单位返回该项所占的内存大小。

除异常外,首先会判断 size 是否超过 maxSize,如果超过了就取出最先插入的缓存,如果不为空就删掉,并把 size 减去该项所占的大小。这个操作将一直循环下去,直到 size 比 maxSize 小或者缓存为空。

一般来说最大值的1/8左右就可以了。

1.8 数据类型

1.8.1 String 是最基本的数据类型吗?

基本数据类型包括 byte、int、char、long、float、double、boolean 和 short。java.lang.String 类是 final 类型的,因此不可以继承这个类、不能修改这个类。为了提高效率节省空间,我们应该用 StringBuffer 类。

1.8.2 int 和 Integer 有什么区别

Java 提供两种不同的类型:引用类型和原始类型(或内置类型)。Int 是 java 的原始数据类型,Integer 是 java 为 int 提供的封装类。Java 为每个原始类型提供了封装类。原始类型封装类:booleanBoolean、charCharacter、byteByte、shortShort、intInteger、longLong、floatFloat、doubleDouble

引用类型和原始类型的行为完全不同,并且它们具有不同的语义。引用类型和原始类型具有不同的特征和用法,它们包括:大小和速度问题,这种类型以哪种类型的数据结构存储,当引用类型和原始类型用作某个类的实例数据时所指定的缺省值。对象引用实例变量的缺省值为 null,而原始类型实例变量的缺省值与它们的类型有关。

1.8.3 String 和 StringBuffer 的区别

Java 平台提供了两个类:String 和 StringBuffer,它们可以储存和操作字符串,即包含多个字符的字符数据。这个 String 类提供了数值不可改变的字符串。而这个 StringBuffer 类提供的字符串进行修改。当你知道字符数据要改变的时候你就可以使用 StringBuffer。典型地,你可以使用 StringBuffers 来动态构造字符数据。

1.9 Final关键字的用法

Final 可以修饰类、变量和方法。修饰类代表这个类不可被继承。修饰变量代表此变量不可被改变。修饰方法表示此方法不可被重写 (override)。

二、基本组件

2.1 Android 中的四大组件以及应用场景

  • Activity:在 Android 应用中负责与用户交互的组件
  • Service:常用于为其他组件提供后台服务或者监控其他组件的运行状态。经常用来执行一些耗时操作
  • BroadcastReceiver:用于监听应用程序中的其他组件
  • ContentProvider:Android 应用程序之间实现实时数据交换

2.2 Manifest.xml 的里有什么?

  • 包名:版本号 package,versionCode,versionName,注意可被 build.gradle 覆盖
  • 权限:程序中用到的权限都要在这里列出,uses-permission
  • 应用程序 Application:里面包括: <meta-data>及三大组件<activity><service><receiver>

2.3 Activity 的启动模式

  • Standard: 标准模式,一调用 startActivity() 方法就会产生一个新的实例
  • SingleTop: 如果已经有一个实例位于 Activity 栈的顶部时,就不产生新的实例,而只是调用 Activity 中的 newInstance() 方法。如果不位于栈顶,会产生一个新的实例
  • SingleTask: 会在一个新的 task 中产生这个实例,以后每次调用都会使用这个,不会去产生新的实例了
  • SingleInstance: 这个跟 singleTask 基本上是一样,只有一个区别:在这个模式下的 Activity 实例所处的 task 中,只能有这个 activity 实例,不能有其他的实例

2.4 Service

Service 分为两种:本地服务和远程服务:

  • 本地服务:属于同一个应用程序,通过 startService 来启动或者通过 bindService 来绑定并且获取代理对象。如果只是想开个服务在后台运行的话,直接 startService 即可,如果需要相互之间进行传值或者操作的话,就应该通过 bindService
  • 远程服务(不同应用程序之间):通过 bindService 来绑定并且获取代理对象

Service的生命周期(start与bind):在 Service 的生命周期中,被回调的方法比 Activity 少一些,只有 onCreate, onStart, onDestroy,onBind 和 onUnbind。

通常有两种方式启动一个 Service,他们对 Service 生命周期的影响是不一样的。

  • 通过 startService:Service 会经历 onCreate 到 onStart,然后处于运行状态,stopService 的时候调用 onDestroy 方法。如果是调用者自己直接退出而没有调用 stopService 的话,Service 会一直在后台运行
  • 通过 bindService:Service 会运行 onCreate,然后是调用 onBind,这个时候调用者和 Service 绑定在一起。调用者退出了,Srevice 就会调用 onUnbind->onDestroyed 方法。所谓绑定在一起就共存亡了。调用者也可以通过调用 unbindService 方法来停止服务,这时候 Service 就会调用 onUnbindonUnbind->onDestroyed 方法

2.5 Activity,Intent,Service 是什么关系

Intent 是 activity 和 service 的桥梁,通信员,activity 主要操作显示界面, service 在后台运行,适合长时间运行,如下载,听歌等。

2.6 Activity 与 Fragment 之间的传值

通过 findFragmentByTag 或者 getActivity 获得对方的引用(强转)之后,再相互调用对方的 public 方法,但是这样做一是引入了“强转”的丑陋代码,另外两个类之间各自持有对方的强引用,耦合较大,容易造成内存泄漏。

通过 Bundle 的方法进行传值,例如以下代码:

//Activity中对fragment设置一些参数
fragment.setArguments(bundle);
//fragment中通过getArguments获得Activity中的方法
Bundle arguments = getArguments();

利用 eventbus 进行通信,这种方法实时性高,而且 Activity 与 Fragment 之间可以完全解耦。

//Activity中的代码
EventBus.getDefault().post("消息");

//Fragment中的代码
EventBus.getDefault().register(this);

@Subscribe
public void test(String text) {
    tv_test.setText(text);
}

2.7 Android 中 Context 的理解

Context:包含上下文信息(外部值) 的一个参数. Android 中的 Context 分三种,Application Context ,Activity Context ,Service Context。它描述的是一个应用程序环境的信息,通过它我们可以获取应用程序的资源和类,也包括一些应用级别操作,例如:启动一个 Activity,发送广播,接受 Intent 信息等。

2.8 res/raw 和 asserts 的区别

这两个目录下的文件都会被打包进 APK,并且不经过任何的压缩处理。

assets 与 res/raw 不同点在于,assets 支持任意深度的子目录,这些文件不会生成任何资源 ID,只能使用 AssetManager 按相对的路径读取文件。如需访问原始文件名和文件层次结构,则可以考虑将某些资源保存在 assets 目录下。

三、UI

3.1 对布局优化的理解

布局的优化其实说白了就是减少层级,越简单越好,减少 Overdraw,就能更好的突出性能。下面介绍几种布局优化的方式:

  • 善用相对布局 Relativelayout
  • 布局优化的另外一种手段就是使用抽象布局标签 include、merge、ViewStub
  • Android 最新的布局方式 ConstaintLayout
  • 利用 Android Lint 工具寻求可能优化布局的层次

3.2 View 的加载流程

在 Android中,大家都知道 Activity 是有生命周期的,其实在 View中,View 也是有有生命周期的:

  • view 布局一直贯穿于整个 Android 应用中,不管是 activity 还是 fragment 都给我们提供了一个 view 依附的对象
  • 在自定义 View 后,我们一般都需要在 xml 布局文件中进行配置。此时,当我们启动 Activity 后,view 显示出来,此时 View 的构造方法会先被调用
  • 当构造方法被调用后,紧接着 View 的 onFinishInflate() 方法被回调。此方法的回调时机是在当 xml 布局中我们的 View 被解析完成后。即这个回调方法是跟 xml 有关系,解析完成则回调,那么假设现在我们直接将 view 在 Acitivity 中创建显示,不经过 xml 布局,这个方法还会不会被回调呢?答案肯定是否定的
  • 接下来紧接着调用的是 onAttachedToWindow 方法,顾名思义,即我们自定义的 view 已创建并添加到 Window 中。该方法后紧接着一般会调用 onWindowVisibilityChanged 方法,这个方法即当 Window 中的 View 的可见性状态(Gone,Visiable,inVisiable)发生改变都会被回调,此时,代表着我们的 View 是被显示出来了
  • View 显示出来后,onMeasure 方法会被回调进行测量,如果尺寸是被确定了,那么会先调用 onSizeChanged 方法通知 View 尺寸大小发生了改变。有子元素,onLayout 方法将被回调来进行布局,然后再次调用 onMeasure 方法对 View 进行二次测量,如果测量值与上一次相同则不再调用 onSizeChanged 方法,接着再次调用 onLayout 方法,如果测量过程结束,则会调用 onDraw 方法绘制 View。

3.3 Bitmap 三级缓存的大致思想与逻辑

所谓三级缓存就是内存缓存、本地缓存(磁盘缓存)、网络缓存。三级缓存的原理就是当 APP 需要引用缓存时,首先到内存缓存中读取,读取不到再到本地缓存中读取,还获取不到就到网络异步读取,读取成功之后再保存到内存和本地缓存中。

3.4 ListView 复用的原理及分页思想

ListView 是我们经常使用的一个控件,虽然说都会用,但是却并不一定完全清楚 ListView 的复用机制,虽然在 Android 5.0 版本之后提供了 RecycleView 去替代 ListView 和GridView,提供了一种插拔式的体验,也就是所谓的模块化。

ListView 优化主要有下面几个方面:

  • convertView 重用
  • ViewHolder 的子 View 复用
  • 缓存数据复用

ListView 分页目的:
Android 应用开发中,采用 ListView 组件来展示数据是很常用的功能,当一个应用要展现很多的数据时,一般情况下都不会把所有的数据一次就展示出来,而是通过 分页的形式来展示数据,这样会有更好的用户体验。因此,很多应用都是采用分批次加载的形式来获取用户所需的数据。例如:微博客户端可能会在用户滑 动至列表底端时自动加载下一页数据,也可能在底部放置一个"查看更多"按钮,用户点击后,加载下一页数据。

核心技术点:

  • 借助 ListView 组件的 OnScrollListener 监听事件,去判断何时该加载新数据
  • 往服务器 get 传递表示页码的参数:page。而该page会每加载一屏数据后自动加一
  • 利用 addAll() 方法不断往 list 集合末端添加新数据,使得适配器的数据源每新加载一屏数据就发生变化
  • 利用适配器对象的 notifyDataSetChanged() 方法。该方法的作用是通知适配器自己及与该数据有关的 view,数据已经发生变动,要刷新自己、更新数据

3.5 要做一个尽可能流畅的 ListView,中如何进行优化的?

  • Item 布局,层级越少越好,使用 hierarchyview 工具查看优化。
  • 复用 convertView
  • 使用 ViewHolder
  • item 中有图片时,异步加载
  • 快速滑动时,不加载图片
  • item 中有图片时,应对图片进行适当压缩
  • 实现数据的分页加载

3.6 介实现一个自定义 view 的基本流程

  • 自定义 View 的属性 编写 attr.xml 文件
  • 在 layout 布局文件中引用,同时引用命名空间
  • 在 View 的构造方法中获得我们自定义的属性 ,在自定义控件中进行读取(构造方法拿到 attr.xml 文件值)
  • 重写 onMesure
  • 重写 onDraw

四、进程、线程

要想知道如何使用多进程,先要知道 Android 里的多进程概念。一般情况下,一个应用程序就是一个进程,这个进程名称就是应用程序包名。我们知道进程是系统分配资源和调度的基本单位,所以每个进程都有自己独立的资源和内存空间,别的进程是不能任意访问其他进程的内存和资源的。

4.1 Android 中线程与线程,进程与进程之间如何通信

一个 Android 程序开始运行时,会单独启动一个 Process。

  • 默认情况下,所有这个程序中的 Activity 或者 Service 都会跑在这个 Process
  • 默认情况下,一个 Android 程序也只有一个 Process,但一个 Process 下却可以有许多个 Thread

一个 Android 程序开始运行时,就有一个主线程 Main Thread被创建。该线程主要负责 UI 界面的显示、更新和控件交互,所以又叫 UI Thread。一个 Android 程序创建之初,一个Process呈现的是单线程模型--即 Main Thread,所有的任务都在一个线程中运行。所以,Main Thread 所调用的每一个函数,其耗时应该越短越好。而对于比较费时的工作,应该设法交给子线程去做,以避免阻塞主线程(主线程被阻塞,会导致程序假死 现象)。

Java 中的线程有四种状态分别是:运行、就绪、挂起、结束。

Android 单线程模型:Android UI 操作并不是线程安全的并且这些操作必须在 UI 线程中执行。如果在子线程中直接修改UI,会导致异常。

4.2 多线程还是多进程的选择及区别

对比度 多进程 多线程 总结
数据共享、同步 数据共享复杂,需要用 IPC;数据是分开的,同步简单 因为共享进程数据,数据共享简单,但也是因为这个原因导致同步复杂 各有优势
内存、CPU 占用内存多,切换复杂,CPU 利用率低 占用内存少,切换简单,CPU 利用率高 线程占优
创建销毁、切换 创建销毁、切换复杂,速度慢 创建销毁、切换简单,速度很快 线程占优
编程、调试 编程简单,调试简单 编程复杂,调试复杂 进程占优
可靠性 进程间不会互相影响 一个线程挂掉将导致整个进程挂掉 进程占优
分布式 适应于多核、多机分布式;如果一台机器不够,扩展到多台机器比较简单 适应于多核分布式 进程占优

4.3 Handle 的使用与原理

Android 之所以提供 Handler,就是为了解决子线程访问 UI 的问题。因为屏幕的刷新频率是 60Hz,大概 16 毫秒会刷新一次,所以为了保证 UI 的流畅性,耗时操作需要在子线程中处理,子线程不能直接对 UI 进行更新操作。因此需要 Handler 在子线程发消息给主线程来更新UI。

andriod 提供了 Handler 和 Looper 来满足线程间的通信。Handler 先进先出原则。Looper 类用来管理特定线程内对象之间的消息交换(Message Exchange)。

  • Looper: 一个线程可以产生一个 Looper 对象,由它来管理此线程里的 Message Queue(消息队列)
  • Handler: 你可以构造 Handler 对象来与 Looper 沟通,以便 push 新消息到 Message Queue 里;或者接收 Looper 从 Message Queue 取出)所送来的消息
  • Message Queue(消息队列):用来存放线程放入的消息
  • 线程:UI thread 通常就是 main threa,而 Android 启动程序时会替它建立一个 Message Queue

4.4 AIDL 的使用与原理

AIDL(Android 接口描述语言)是一种接口描述语言; 编译器可以通过 aidl 文件生成一段代码,通过预先定义的接口达到两个进程内部通信进程的目的;如果需要在一个 Activity 中, 访问另一个 Service 中的某个对象, 需要先将对象转化成 AIDL 可识别的参数(可能是多个参数), 然后使用 AIDL 来传递这些参数, 在消息的接收端, 使用这些参数组装成自己需要的对象。AIDL 是基于接口的,但它是轻量级的。它使用代理类在客户端和实现层间传递值。

4.5 线程种类区别

Java 线程一共分成两种,用户线程和守护线程。

  • 用户线程:是用户创建的一般线程,如继承 Thread 类或实现 Runnable 接口等实现的线程
  • 守护线程:是为用户线程提供服务的线程,如 JVM 的垃圾回收、内存管理等线程

守护线程和用户线程的区别:当一个用户线程结束后,JVM 会检查系统中是否还存在其他用户线程,如果存在则按照正常的调用方法调用。但是如果只剩守护线程而没有用户线程的话,JVM 就会终止(从始至终都没有理睬守护线程)。

  • 任何线程都可以是守护线程或者用户线程,所有线程一开始都是用户线程
  • 涉及守护线程的方法有两个:setDaemon( ) 和 isDaemon()
  • Thread.setDaemon(false/true) 设置为用户线程/守护线程;如果不设置该属性,默认为 false

需要注意的是:setDaemon() 方法仅仅在线程对象已经被创建但是还没有运行前才能被调用,否则会报错。需要注意的是:setDaemon() 方法仅仅在线程对象已经被创建但是还没有运行前才能被调用,否则会报错。

4.6 几种创建线程的方式

Java 中创建线程主要有三种方式:

4.6.1 继承 Thread 类创建线程类
  • 定义 Thread 类的子类,并重写该类的run方法,该 run 方法的方法体就代表了线程要完成的任务。因此把 run() 方法称为执行体
  • 创建 Thread 子类的实例,即创建了线程对象
  • 调用线程对象的 start() 方法来启动该线程
// 继承Thread类
class Thread1 extends Thread {

    @Override
    public void run() {
        Thread.currentThread().setName("one");// 设置线程的名字
        System.out.println("thread1 name:" + Thread.currentThread().getName());
        System.out.println("thread1 priority:" + Thread.currentThread().getPriority());
        System.out.println("thread1 id:" + Thread.currentThread().getId());
    }
}



// 线程1
    Thread1 t1 = new Thread1();
    t1.start();

4.6.2 通过 Runnable 接口创建线程类

同上,只是改为定义 runnable 接口的实现类。
启动 new Thread(Runnable r,String name).start();

// 实现Runnable接口,没有返回类型,重写run()方法
class Thread2 implements Runnable {

    @Override
    public void run() {
        System.out.println("thread2 name:" + Thread.currentThread().getName());
        System.out.println("thread2 priority:" + Thread.currentThread().getPriority());
        System.out.println("thread2 id:" + Thread.currentThread().getId());
    }
}


// 线程2:必须创建Thread类的实例,通过Thread类的构造方法函数将Runnable对象转为Thread对象,
    Thread2 t2 = new Thread2();
    Thread t = new Thread(t2);
    t.start();

4.6.3 通过 Callable 和 FutureTask 创建线程
  • 创建 Callable 接口的实现类,并实现 call() 方法,该 call() 方法将作为线程执行体,并且有返回值
  • 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了该 Callable 对象的 call() 方法的返回值
  • 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动新线程
  • 调用 FutureTask 对象的 get() 方法来获得子线程执行结束后的返回值
/**
 * 实现Callable接口,有返回类型 ,重写call()方法
 * 
 * @author wxb
 *
 */
class Thread3 implements Callable<Integer> {

    @Override
    public Integer call() throws Exception {
        int sum = 100;
        return Integer.valueOf(sum);
    }

}


// 线程3:创建Future对象,再使用Thread类的构造方法去完成
    Thread3 t3 = new Thread3();
    FutureTask<Integer> task = new FutureTask<>(t3);
    Thread thread = new Thread(task);
    thread.start();

4.7 线程池的种类与作用

线程池作用就是限制系统中执行线程的数量,线程池使用场景:

  • 单个任务处理时间短
  • 将需处理的任务数量大

使用 new Thread() 创建线程的弊端:

  • 每次通过 new Thread() 创建对象性能不佳
  • 线程缺乏统一管理,可能无限制新建线程,相互之间竞争,及可能占用过多系统资源导致死机或OOM
  • 缺乏更多功能,如定时执行、定期执行、线程中断

使用 Java 线程池的好处:

  • 重用存在的线程,减少对象创建、消亡的开销,提升性能
  • 可有效控制最大并发线程数,提高系统资源的使用率,同时避免过多资源竞争,避免堵塞
  • 提供定时执行、定期执行、单线程、并发数控制等功能
4.7.1 newCachedThreadPool

作用:创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们,并在需要时使用提供的 ThreadFactory 创建新线程。

特征:

  • 线程池中数量没有固定,可达到最大值(Interger. MAX_VALUE)
  • 线程池中的线程可进行缓存重复利用和回收(回收默认时间为1分钟)
  • 当线程池中,没有可用线程,会重新创建一个线程

创建方式: Executors.newCachedThreadPool();

  • 底层:返回 ThreadPoolExecutor 实例,corePoolSize 为 0;maximumPoolSize 为I nteger.MAX_VALUE;keepAliveTime 为 60L;unit 为 TimeUnit.SECONDS;workQueue 为 SynchronousQueue(同步队列)
  • 通俗:当有新任务到来,则插入到 SynchronousQueue 中,由于 SynchronousQueue 是同步队列,因此会在池中寻找可用线程来执行,若有可以线程则执行,若没有可用线程则创建一个线程来执行该任务;若池中线程空闲时间超过指定大小,则该线程会被销毁
  • 适用:执行很多短期异步的小程序或者负载较轻的服务器
4.7.2 newFixedThreadPool

作用:创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程。在任意点,在大多数 nThreads 线程会处于处理任务的活动状态。如果在所有线程处于活动状态时提交附加任务,则在有可用线程之前,附加任务将在队列中等待。如果在关闭前的执行期间由于失败而导致任何线程终止,那么一个新线程将代替它执行后续的任务(如果需要)。在某个线程被显式地关闭之前,池中的线程将一直存在。

特征:

  • 线程池中的线程处于一定的量,可以很好的控制线程的并发量
  • 线程可以重复被使用,在显示关闭之前,都将一直存在
  • 超出一定量的线程被提交时候需在队列中等待

创建方式:

  • Executors.newFixedThreadPool(int nThreads);//nThreads为线程的数量
  • Executors.newFixedThreadPool(int nThreads,ThreadFactory threadFactory);//nThreads 为线程的数量,threadFactory 创建线程的工厂方式

  • 底层:返回 ThreadPoolExecutor 实例,接收参数为所设定线程数量 nThread,corePoolSize 为 nThread,maximumPoolSize 为 nThread;keepAliveTime 为 0L(不限时);unit 为:TimeUnit.MILLISECONDS;WorkQueue 为:new LinkedBlockingQueue< Runnable>() 无解阻塞队列

  • 通俗:创建可容纳固定数量线程的池子,每隔线程的存活时间是无限的,当池子满了就不在添加线程了;如果池中的所有线程均在繁忙状态,对于新任务会进入阻塞队列中(无界的阻塞队列)
  • 适用:执行长期的任务,性能好很多
4.7.3 newSingleThreadExecutor

作用:创建一个使用单个 worker 线程的 Executor,以无界队列方式来运行该线程。(注意,如果因为在关闭前的执行期间出现失败而终止了此单个线程,那么如果需要,一个新线程将代替它执行后续的任务)。可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。与其他等效的 newFixedThreadPool(1) 不同,可保证无需重新配置此方法所返回的执行程序即可使用其他的线程。

特征:

  • 线程池中最多执行 1 个线程,之后提交的线程活动将会排在队列中以此执行

创建方式:

  • Executors.newSingleThreadExecutor() ;
  • Executors.newSingleThreadExecutor(ThreadFactory threadFactory);// threadFactory创建线程的工厂方式

  • 底层:FinalizableDelegatedExecutorService 包装的 ThreadPoolExecutor 实例,corePoolSize 为 1;maximumPoolSize 为 1;keepAliveTime 为 0L;unit为:TimeUnit.MILLISECONDS;workQueue 为:new LinkedBlockingQueue< Runnable>() 无解阻塞队列

  • 通俗:创建只有一个线程的线程池,且线程的存活时间是无限的;当该线程正繁忙时,对于新任务会进入阻塞队列中(无界的阻塞队列)
  • 适用:一个任务一个任务执行的场景
4.7.4 newScheduleThreadPool

作用: 创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。

特征:

  • 线程池中具有指定数量的线程,即便是空线程也将保留
  • 可定时或者延迟执行线程活动

创建方式:

  • Executors.newScheduledThreadPool(int corePoolSize);// corePoolSize线程的个数
  • newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory);// corePoolSize线程的个数,threadFactory创建线程的工厂

  • 底层:创建 ScheduledThreadPoolExecutor 实例,corePoolSize 为传递来的参数,maximumPoolSize 为 Integer.MAX_VALUE;keepAliveTime 为 0;unit 为:TimeUnit.NANOSECONDS;workQueue 为:new DelayedWorkQueue() 一个按超时时间升序排序的队列

  • 通俗:创建一个固定大小的线程池,线程池内线程存活时间无限制,线程池可以支持定时及周期性任务执行,如果所有线程均处于繁忙状态,对于新任务会进入 DelayedWorkQueue 队列中,这是一种按照超时时间排序的队列结构
  • 适用:周期性执行任务的场景
4.7.5 newSingleThreadScheduledExecutor

作用: 创建一个单线程执行程序,它可安排在给定延迟后运行命令或者定期地执行。

特征:

  • 线程池中最多执行1个线程,之后提交的线程活动将会排在队列中以此执行
  • 可定时或者延迟执行线程活动

创建方式:

  • Executors.newSingleThreadScheduledExecutor() ;
  • Executors.newSingleThreadScheduledExecutor(ThreadFactory threadFactory) ;//threadFactory创建线程的工厂

线程池任务执行流程:

  • 当线程池小于 corePoolSize 时,新提交任务将创建一个新线程执行任务,即使此时线程池中存在空闲线程。
  • 当线程池达到 corePoolSize 时,新提交任务将被放入 workQueue 中,等待线程池中任务调度执行
  • 当 workQueue 已满,且 maximumPoolSize>corePoolSize 时,新提交任务会创建新线程执行任务
  • 当提交任务数超过 maximumPoolSize 时,新提交任务由 RejectedExecutionHandler 处理
  • 当线程池中超过 corePoolSize 线程,空闲时间达到 keepAliveTime 时,关闭空闲线程
  • 当设置 allowCoreThreadTimeOut(true) 时,线程池中 corePoolSize 线程空闲时间达到 keepAliveTime 也将关闭

4.8 Android 中的其他线程与 Main 线程通讯有哪些

Android 中的线程延续了 Java 的设计模型,默认一个应用程序只有一个主线程,主线程的开启是在 Activity 的 main() 方法。Android 中的 Main 线程的事件处理不能太耗时,否则后续的事件无法在 5 秒内得到响应,就会弹出ANR对话框。在 Android 中有多种方法可以实现其他线程与 Main 线程通讯,我们这里介绍常见有:

使用 AsyncTask:AsyncTask 是 Android 框架提供的异步处理的辅助类,它可以实现耗时操作在其他线程执行,而处理结果在 Main 线程执行,对于开发 者而言,它屏蔽掉了多线程和后面要讲的 Handler 的概念。你不了解怎么处理线程间通讯也没有关系,AsyncTask 体贴的帮你做好了。使用他你会发 现你的代码很容易被理解,因为他们都有一些具有特定职责的方法,尤其是 AsyncTask,有预处理的方法 onPreExecute,有后台执行任务的方法 doInBackground,有更新进度的方法 publishProgress,有返回结果的方法 onPostExecute 等等,这就不像 post 这些方法,把所有的操作都写在一个 Runnable 里。不过封装越好越高级的 API,对初级程序员反而越不利,就是你不了解它的原理。当你需要面对更加复 杂的情况,而高级 API 无法完成得很好时,你就杯具了。所以,我们也要掌握功能更强大,更自由的与 Main 线程通讯的方法:Handler 的使用。

使用 Handler:这里需要了解 Android SDK 提供的几个线程间通讯的类。

  • Handler:Handler 在 android 里负责发送和处理消息,通过它可以实现其他线程与 Main 线程之间的消息通讯
  • Looper:Looper 负责管理线程的消息队列和消息循环
  • Message:Message 是线程间通讯的消息载体。两个码头之间运输货物,Message 充当集装箱的功能,里面可以存放任何你想要传递的消息
  • MessageQueue:MessageQueue 是消息队列,先进先出,它的作用是保存有待线程处理的消息

它们四者之间的关系是,在其他线程中调用H andler.sendMsg() 方法(参数是Message对象),将需要 Main 线程处理的事件 添加到 Main 线程的 MessageQueue 中,Main 线程通过MainLooper 从消息队列中取出 Handler 发过来的这个消息时,会回调 Handler 的 handlerMessage() 方法。

Activity.runOnUiThread(Runnable)

View.post(Runnable),View.postDelayed(Runnable, long)

Handler.post,Handler.postDelayed(Runnable, long)

4.9 ANR

不同的组件发生 ANR 的时间不一样,主线程(Activity、Service)是 5 秒,BroadCastReceiver 是 10 秒。

ANR 一般有三种类型:

  • KeyDispatchTimeout(5 seconds):主要类型按键或触摸事件在特定时间内无响应
  • BroadcastTimeout(10 seconds):BroadcastReceiver 在特定时间内无法处理完成
  • ServiceTimeout(20 seconds):小概率类型 Service 在特定的时间内无法处理完成

解决方案:

  • UI 线程只进行 UI 相关的操作。所有耗时操作,比如访问网络,Socket 通信,查询大量 SQL 语句,复杂逻辑计算等都放在子线程中去,然后通过 handler.sendMessage、runonUITread、AsyncTask 等方式更新 UI
  • 无论如何都要确保用户界面操作的流畅度。如果耗时操作需要让用户等待,那么可以在界面上显示进度条
  • BroadCastReceiver 要进行复杂操作的的时候,可以在 onReceive() 方法中启动一个 Service 来处理

4.10 Android 中进程间通信

Intent,Binder(AIDL),Messenger,BroadcastReceiver.

Binder 是一种 IPC 机制,进程间通讯的一种工具,Java 层可以利用 aidl 工具来实现相应的接口。

五、文件与数据库

5.1 Android 的数据存储方式有哪些

Android 中有 5 种数据存储方式,分别为 文件存储、SQLite数据库、SharedPreferences、ContentProvider、网络。每种存储方式的特点如下:

  • 文件存储:文件存储方式是一种较常用的方法,在 Android 中读取/写入文件的方法,与 Java 中实现 I/O 的程序是完全一样的,提供 openFileInput() 和 openFileOutput() 方法来读取设备上的文件
  • SQLite 数据库:SQLite 是 Android 所集成的一个轻量级的嵌入式数据库,它不仅可以使用 Andorid API 操作,同时它也支持 SQL 语句进行增删改查等操作
  • SharedPreferences:SharedPreferences 是 Android 提供的用于存储一些简单配置信息的一种机制,采用了 XML 格式将数据存储到设备中。不仅可以在同一个包下使用,还可以访问其他应用程序的数据,但是由于 SharedPreferences 的局限性,在实际操作中很少用来读取其他应用程序的数据
  • ContentProvider:ContentProvider 主要用于不同应用程序之间共享数据,ContentProvider 更好的提供了数据共享接口的统一性,使不同应用共享数据更规范和安全
  • 网络存储数据:通过网络上提供的存储空间来上传(存储)或下载(获取)我们存储在网络空间中的数据信息

5.2 Android 的 SQLite 中要继承那个类来创建与更新数据库

SQLite 是一款轻量级的关系型数据库,它的运算速度非常快,占用资源很少,通常只需要几百 K 的内存就足够了,因而特别适合在移动设备上使用。

  • 自己写一个类继承自 SqliteOpenHelper
  • 会实现 SqliteOpenHelper 的两个方法 onCreate 与 onUpgrade,google 文档对两个回调方法的解释是创建数据库的时候调用与更新数据库的版本的时候调用
  • Sqlite 数据库主要是用来缓存应用的数据,而应用却是一直在更新版本,相应的数据的表的字段也会一直增加会改变或减少
  • 这个时候就需要控制数据库的版本,因为 Sqlite 数据库中的字段假设新版的应用里面设计的表是 10个 字段,而缓存却是之前缓存的只有 9 个字段的话,查询数据库之后的列,然后取的值会出现空指针异常或报错
  • 所以 Android 中引入了 Sqlite 数据库的版本,让应用的旧版数据库能够与新版的数据库的字段兼容
  • 为了兼容之前的数据库的版本,只需要在应用的版本更新的时候,添加字段或者删除字段即可
  • 你开发程序当前是 1.0.0 的版本,该程序用到了数据库,但是版本迭代之后到 1.0.1 的时候,数据库的某个表添加了某个字段在软件 1.0.1 的版本就需要升级
  • 数据库升级可以为了能够让旧的数据不能丢,所以不能删除掉之前数据库中的所有数据,那么就需要有地方能够检测到版本的变化,这个跟 Android 的 APP 升级是一个道理,当然这个检测就是在 SqliteOpenHelper 的 onUpgrade 方法中

六、网络相关

6.1 HTTP的结构有那些

超文本传输协议(HTTP,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络协议。所有的WWW文件都必须遵守这个标准。设计 HTTP 最初的目的是为了提供一种发布和接收 HTML 页面的方法。

HTTP 协议采用了请求/响应模型。客户端向服务器发送一个请求,请求头包含请求的方法、URL、协议版本、以及包含请求修饰符、客户信息和内容的类似于 MIME 的消息结构。服务器以一个状态行作为响应,响应的内容包括消息协议的版本,成功或者错误编码加上包含服务器信息、实体元信息以及可能的实体内容。

通常 HTTP 消息包括客户机向服务器的请求消息和服务器向客户机的响应消息。这两种类型的消息由一个起始行,一个或者多个头域,一个指示头域结束的空行和可选的消息体组成。HTTP 的头域包括通用头,请求头,响应头和实体头四个部分。每个头域由一个域名,冒号(:)和域值三部分组成。域名是大小写无关的,域值前可以添加任何数量的空格符,头域可以被扩展为多行,在每行开始处,使用至少一个空格或制表符。

6.2 Session 与 Cookie 及 Token 的区别

这三种东西诞生的背景因为 http 为无状态协议,就是说浏览器这一步请求并不知道上一步请求所包含的状态数据。如何把用户的数据请求关联起来就成了关键。

cookie 的出现:服务器发送给浏览器的,被浏览器保存,当有 http 请求时,就发送这个 cookie 给服务器。

缺陷:客户端就能修改数据,不能存放重要数据,当 cookie 中的数据字段过多就会影响传输效率。

session 的出现:session 是放在服务器端的,其运作是通过 session_id 进行的,session_id 在第一次被访问的时候就被存放在 cookie 中,当你下次访问的时候,cookie 带着session_id,服务器就知道你访问过哪里,并将 session_id 和服务器端的 session data 关联起来,进行数据保存和修改。

token 的出现:令牌,用于验证表明身份的数据或口令数据,可以用 post、get、夹在 http 中的 header 中。判断你是否已经对该文件授权。token 用在两个地方:一是表单重复提交,二是 anti csrf 攻击。cookie 记录了 token 和 u_id 两个字段。

6.3 HTTP 与 HTTPS 的区别

Http 协议传输的数据都是未加密的,也就是明文的,因此使用 Http 协议传输隐私信息非常不安全。为了保证这些隐私数据能加密传输,于是网景公司设计了 ssl(Secure Sockets Layer) 协议用于对 http 协议传输的数据进行加密,从而就诞生了 https。简单来说,https 协议是由 ssl + http 协议构建的可进行加密传输、身份认证的网络协议,要比 http 协议安全。

https 和 http 的主要区别:

  • https 协议需要到 ca 机构申请 ssl 证书(如沃通 CA),另外沃通 CA 还提供3年期的免费 ssl 证书,高级别的 ssl 证书需要一定费用
  • http 是超文本传输协议,信息是明文传输,https 则是具有安全性的 ssl 加密传输协议
  • http 和 https 使用的是完全不同的连接方式,用的端口也不一样,http 是 80 端口,https 是 443 端口
  • http 的连接很简单,是无状态的;https 协议是由 ssl+http 协议构建的可进行加密传输、身份认证的网络协议,比 http 协议安全
  • 如果要实现 HTTPS,那么可以各平台购买获取 SSL 证书

七 系统

7.1 序列化原因:

  • 永久性保存对象,保存对象的字节序列到本地文件中
  • 通过序列化对象在网络中传递对象
  • 通过序列化在进程间传递对象

Parcelable 比 Serializable 性能高,所以应用内传递数据推荐使用 Parcelable,但是 Parcelable 不能使用在要将数据存储在磁盘上的情况,因为 Parcelable不 能很好的保证数据的持续性在外界有变化的情况下。尽管 Serializable 效率低点,但此时还是建议使用 Serializable。

7.2 冷启动与热启动是什么,区别,如何优化

冷启动:在启动应用时,系统中没有该应用的进程,这时系统会创建一个新的进程分配给该应用;
热启动:在启动应用时,系统中已有该应用的进程(例:按 Back 键、Home 键,应用虽然会退出,但是该应用的进程还是保留在后台);

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

冷启动时间的计算
API 19 之后,系统会出打印日志输出启动的时间; 冷启动时间 = 应用启动(创建进程) —> 完成视图的第一次绘制(Activity 内容对用户可见)。

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

总结:
Application 构造方法 –> attachBaseContext() –> onCreate() –> Activity 构造方法 –> onCreate() –> 配置主题中背景等属性 –> onStart() –> onResume() –> 测量布局绘制显示在界面上。

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

7.3 对于 Android 的安全问题

  • 错误导出组件
  • 参数校验不严
  • WebView 引入各种安全问题,webview 中的 js 注入
  • 不混淆、不防二次打包
  • 明文存储关键信息
  • 错误使用 HTTPS
  • 山寨加密方法
  • 滥用权限、内存泄露、使用 Debug 签名

7.4 死锁是怎么导致的?如何定位死锁

某个任务在等待另一个任务,而后者又等待别的任务,这样一直下去,直到这个链条上的任务又在等待第一个任务释放锁。这得到了一个任务之间互相等待的连续循环,没有哪个线程能继续。这被称之为死锁。当以下四个条件同时满足时,就会产生死锁:

  • 互斥条件。任务所使用的资源中至少有一个是不能共享的
  • 任务必须持有一个资源,同时等待获取另一个被别的任务占有的资源
  • 资源不能被强占
  • 必须有循环等待。一个任务正在等待另一个任务所持有的资源,后者又在等待别的任务所持有的资源,这样一直下去,直到有一个任务在等待第一个任务所持有的资源,使得大家都被锁住

要解决死锁问题,必须打破上面四个条件的其中之一。在程序中,最容易打破的往往是第四个条件。

八、框架

8.1 MVP

任何事务都存在两面性,MVP 当然也不列外,我们来看看MVP的优缺点。

优点:

  • 降低耦合度,实现了 Model 和 View 真正的完全分离,可以修改 View 而不影响 Modle
  • 模块职责划分明显,层次清晰
  • Presenter 可以复用,一个 Presenter 可以用于多个 View,而不需要更改 Presenter 的逻辑(当然是在 View 的改动不影响业务逻辑的前提下)利于测试驱动开发。View 可以进行组件化。在 MVP 当中,View 不依赖 Model。这样就可以让 View 从特定的业务场景中脱离出来,可以说 View 可以做到对业务完全无知。它只需要提供一系列接口提供给上层操作。这样就可以做到高度可复用的View组件
  • 代码灵活性

缺点:

  • Presenter 中除了应用逻辑以外,还有大量的 View->Model,Model->Vie w的手动同步逻辑,造成 Presente r比较笨重,维护起来会比较困难
  • 由于对视图的渲染放在了 Presenter 中,所以视图和 Presenter 的交互会过于频繁
  • 如果 Presenter 过多地渲染了视图,往往会使得它与特定的视图的联系过于紧密。一旦视图需要变更,那么 Presenter 也需要变更了
  • 额外的代码复杂度及学习成本

8.2 组件化方案和思想

组件化开发就是将一个 APP 分成多个模块,每个模块都是一个组件(Module),开发的过程中我们可以让这些组件相互依赖或者单独调试部分组件等,但是最终发布的时候是将这些组件合并统一成一个 APK,这就是组件化开发。

组件中提供 Common 库:

  • 我们将给给整个工程提供统一的依赖第三方库的入口,前面介绍的 Common 库的作用之一就是统一依赖开源库,因为其他业务组件都依赖了 Common 库,所以这些业务组件也就间接依赖了Common 所依赖的开源库
  • 在 Common 组件中我们封装了项目中用到的各种 Base 类,这些基类中就有 BaseApplication 类

8.3 aar 和 jar 的区别

  • aar:.aar 文件中包含所有资源,class 以及 res 资源文件
  • jar:只包含了 class 文件与清单文件 ,不包含资源文件,如图片等所有 res 中的文件

如果只是一个简单的类库那么使用生成的 .jar 文件即可;如果是一个 UI 库,包含一些自己写的控件布局文件以及字体等资源文件那么就只能使用 *.aar 文件。


待续......