APK 组成
Android应用是用Java编写的,利用Android SDK编译代码,并且把所有的数据和资源文件打包成一个APK (Android Package)文件,这是一个后缀名为.apk的压缩文件,APK文件中包含了一个Android应用程序的所有内容,是Android平台用于安装应用程序的文件。
通过 Android Studio 的 APK Analyzer 功能,可以直接打开apk文件,我们可以看到以下的结构:
-
AndroidManifest.xml
描述配置文件 -
classes.dex
是java源码编译后生成的java字节码文件,因方法数限制,可拆分为多个dex.优化重排后生成dex文件,生成的dex文件可以在Dalvik虚拟机执行,且速度比较快 -
META-INF
存放签名和证书的目录 -
res
主要是存放图片资源 -
lib
主要是存放so库,各个cpu架构 -
assets
主要存放不需要编译处理的文件 -
resources.arsc
是编译后的二进制资源文件,记录了资源id和资源的对应关系
resource.arsc
安装过程
- 复制APK安装包到
/data/app
下 (系统自带的是在/system/app
),然后校验APK的签名是否正确,检查APK的结构是否正常 - 接着解压并且校验APK中的dex文件,将apk中的dex文件拷贝到
/data/dalvik-cache
目录下,确定dex文件没有被损坏后,Android 会通过一个专门的工具DexOpt
来对dex 文件优化。DexOpt 在第一次加载 dex 文件的时候会生成odex(Optimised Dex) 文件,使得应用程序启动时间加快(主要是因为分离了程序资源和可执行文件)。
dex 文件是 dalvik 虚拟机的可执行文件,ART–Android Runtime 的可执行文件格式为OAT,启用ART时,系统会执行 dex 文件转换至 OAT 文件。
Android5.0开始,默认已经使用ART,弃用Dalvik了,应用程序会在安装时被编译成OAT文件,但是还是会生成ODEX 文件
,主要是因为相比做过ODEX优化,未做过优化的。DEX转成成OAT要花费更长的时间,比如2-3倍。虽然dalvik被弃用了,但是ODEX优化还是非常有用的。首先ODEX优化不仅仅只是针对应用程序,还会对内核镜像,jar库文件等进行优化。其次,资源和可执行文件分离带来的性能提升无论是运行在ART还是Dalvik,都有效。 - 同时在
/data/data
目录下建立于APK包名相同的文件夹,用于存放应用程序的数据,如数据库、xml文件、cache、二进制的so动态库等等。如果APK中有lib库,系统会判断这些so库的名字, 查看是否以lib开头,是否以.so结尾,再根据CPU的架构解压对应的so库到/data/data/packagename/lib
下。 - 解析 apk 的AndroidManifinest.xml文件。系统在安装apk的过程中,会解析apk的AndroidManifinest.xml文件,提取出这个apk的重要信息写入到
/data/system/packages.xml
文件中,这些信息包括:权限、应用包名、APK的安装位置、版本、userID等等。
odex文件格式
要注意的是, 安装过程并没有把资源文件, assets目录下文件拷贝出来,他们还在apk包里面呆着,所以,当应用要访问资源的时候,其实是从apk包里读取出来的。其过程是,首先加载apk里的resources(这个文件是存储资源Id与值的映射文件),根据资源id读取加载相应的资源。
加载
因为是java 程序,使用的是双亲委托模型,深入理解 JVM (二)中有讲解
一个加载apk 的例子
final File apkFile = new File(Environment.getExternalStorageDirectory().getPath() + File.separator + "xxx.apk");
DexFile dx = DexFile.loadDex(apkFile.getAbsolutePath(),
File.createTempFile("opt", "dex", getApplicationContext().getCacheDir()).getPath(),
0);
DexClassLoader dexClassLoader = new DexClassLoader(
apkFile.getAbsolutePath(),
getExternalCacheDir().getAbsolutePath(),
null,
getClassLoader());
// 加载 com.test.IDexTestImpl 类
Class clazz = dexClassLoader.loadClass("com.test.IDexTestImpl");
Object dexTest = clazz.newInstance();
Method getText = clazz.getMethod("getText");
final String result = getText.invoke(dexTest).toString();
mHandler.post(new Runnable() {
@Override
public void run() {
pd.dismiss();
if (!TextUtils.isEmpty(result)) {
tv.setText(result);
}
}
});
大体过程是先创建一个 DexFile
和一个 DexClassLoader
, 这个 ClassLoader 就可以 load 里面响应的类
android 下的ClassLoader
classLoader间的关系-
URLClassLoader
只能用来加载jar文件,在android的Dal/ART上是没法使用的 -
InMemoryDexClassLoader
可以从一个包含Dex文件的buffer中加载class,可以用在还没写入本地文件系统的情况下 -
PathClassLoader
只能加载本地的dex或apk文件,而不能加载网络上的
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String libraryPath,
ClassLoader parent) {
super(dexPath, null, libraryPath, parent);
}
}
-
DexClassLoader
对于文件的加载灵活的多,可以从SD卡上加载包含class.dex的jar和apk文件,也是插件化和热修复的基础,在不需要安装应用的情况下,完成需要使用的dex的加载。
我们来看一下它们两个的父类 BaseDexClassLoader
public BaseDexClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(parent);
this.originalPath = dexPath;
this.pathList =
new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
这个类里面维护了一个 DexPathList
final class DexPathList {
public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
...
this.definingContext = definingContext;
this.dexElements =
makeDexElements(splitDexPath(dexPath), optimizedDirectory);
this.nativeLibraryDirectories = splitLibraryPath(libraryPath);
}
private static ArrayList<File> splitDexPath(String path) {
return splitPaths(path, null, false);
}
private static ArrayList<File> splitPaths(String path1, String path2, boolean wantDirectories) {
ArrayList<File> result = new ArrayList<File>();
splitAndAdd(path1, wantDirectories, result);
splitAndAdd(path2, wantDirectories, result);
return result;
}
// 过滤掉不可读的file和不存在的file
private static void splitAndAdd(String path, boolean wantDirectories,
ArrayList<File> resultList) {
...
String[] strings = path.split(Pattern.quote(File.pathSeparator));
for (String s : strings) {
File file = new File(s);
if (!(file.exists() && file.canRead())) {
continue;
}
if (wantDirectories) {
if (!file.isDirectory()) {
continue;
}
} else {
if (!file.isFile()) {
continue;
}
}
resultList.add(file);
}
}
// 返回一个 Element 数组,Element是 dex或 zip 构成
private static Element[] makeDexElements(ArrayList<File> files,
File optimizedDirectory) {
ArrayList<Element> elements = new ArrayList<Element>();
for (File file : files) {
ZipFile zip = null;
DexFile dex = null;
String name = file.getName();
if (name.endsWith(DEX_SUFFIX)) { // 以 .dex 结尾
try {
dex = loadDexFile(file, optimizedDirectory); // 返回一个 DexFile
} catch (IOException ex) {
System.logE("Unable to load dex file: " + file, ex);
}
} else if (name.endsWith(APK_SUFFIX) || name.endsWith(JAR_SUFFIX)
|| name.endsWith(ZIP_SUFFIX)) { // 以 .apk .jar .zip 结尾
try {
zip = new ZipFile(file);
} catch (IOException ex) {
System.logE("Unable to open zip file: " + file, ex);
}
try {
dex = loadDexFile(file, optimizedDirectory);
} catch (IOException ignored) {}
} else {
System.logW("Unknown file type for: " + file);
}
if ((zip != null) || (dex != null)) {
elements.add(new Element(file, zip, dex)); //构造成一个 Element
}
}
return elements.toArray(new Element[elements.size()]);
}
}
// BaseDexClassLoader.java
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}
// DexPathList.java
public Class findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
DexFile dex = element.dexFile;
if (dex != null) {
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
所以构造函数就是根据已知文件路径,初始化了一个 Element 列表。
在 DexClassLoader.loadClass
的时候,根据父类委托模型,先去父类加载,等到BaseDexClassLoader
的时候最终会调用 findClass
, 然后委托给 DexPathList.findClass
, 通过遍历 Element
来加载.
Conext相关
Activity、Service和Application这三种类型的Context都是可以通用的,因为他们都是ContextWrapper的子类,但是由于如果是启动Activity,弹出Dialog,一般涉及到"界面"的领域,都只能由Activity来启动。因为出于安全的考虑,Android是不允许Activity或者Dialog凭空出现的,一个Activity的启动必须建立在另一个Activity的基础之上,也就是以此形成了返回栈。而Dialog则必须在一个Activity上面弹出(如果是系统的除外),因此这种情况下我们只能使用Activity类型的Context。整理了一些使用场景的规则,也就是Context的作用域,如下图:
Context作用域
Activity作为Context,作用域最广,是因为Activity继承自ContextThemeWrapper,而Application和Service继承自ContextWrapper。
网友评论