美文网首页
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