美文网首页
Android APK 构成与加载

Android APK 构成与加载

作者: 莫库施勒 | 来源:发表于2019-06-14 16:02 被阅读0次

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

安装过程

  1. 复制APK安装包到 /data/app 下 (系统自带的是在/system/app),然后校验APK的签名是否正确,检查APK的结构是否正常
  2. 接着解压并且校验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,都有效。
  3. 同时在 /data/data 目录下建立于APK包名相同的文件夹,用于存放应用程序的数据,如数据库、xml文件、cache、二进制的so动态库等等。如果APK中有lib库,系统会判断这些so库的名字, 查看是否以lib开头,是否以.so结尾,再根据CPU的架构解压对应的so库到 /data/data/packagename/lib 下。
  4. 解析 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。

相关文章

网友评论

      本文标题:Android APK 构成与加载

      本文链接:https://www.haomeiwen.com/subject/opjpfctx.html