最近几天一直在学习Android APK Dex分包的相关知识,因为Android热修复需要Dex分包,而Android热修复是现在比较火的技术,所以现在将我这几天学到的相关东西做一个总结,这篇主要从AndroidStudio和Eclipse两个方面总结Dex分包的过程。

为什么要Dex分包

当一个app的功能越来越复杂,代码量越来越多,也许有一天便会突然遇到下列现象:
1. 生成的apk在2.3以前的机器无法安装,提示INSTALL_FAILED_DEXOPT
2. 方法数量过多,编译时出错,提示: Conversion to Dalvik format failed:Unable to execute dex: method ID not in [0, 0xffff]: 65536

出现这种问题的原因是:
1. Android2.3及以前版本用来执行dexopt(用于优化dex文件)的内存只分配了5M
2. 一个dex文件最多只支持65536个方法。

Dex分包可以解决上面两个问题,由于AndroidStudio采用Gradle构建项目,Eclipse使用Ant构建项目,所以需要分开说明Dex分包的步骤

AndroidStudio中的Dex分包步骤

AndroidStudio使用Gradle构建项目,所以Dex分包主要是在build.gradle文件中做配置,下面通过一个Demo来说明:

  • 使用AndroidStudio新建Android工程MultiDexDemo
  • 配置app模块的build.gradle文件,代码如下:
apply plugin: 'com.android.application'android {    compileSdkVersion 23    buildToolsVersion "24.0.1"    defaultConfig {        applicationId "com.test.multidexdemo"        minSdkVersion 15        targetSdkVersion 23        versionCode 1        versionName "1.0"        multiDexEnabled true    }    buildTypes {        release {            minifyEnabled true            proguardFiles getDefaultProguardFile('proguard-android.txt'),                    'proguard-rules.pro'        }    }}afterEvaluate {    tasks.matching {        it.name.startsWith('dex')    }.each { dx ->        if (dx.additionalParameters == null) {            dx.additionalParameters = ['--multi-dex']        } else {            dx.additionalParameters += '--multi-dex'        }    }}dependencies {    compile fileTree(dir: 'libs', include: ['*.jar'])    testCompile 'junit:junit:4.12'    compile 'com.android.support:appcompat-v7:23.4.0'    compile 'com.android.support:multidex:1.0.0'}
  • 在AndroidManifest.xml文件中配置Application如下:
 android:name="android.support.multidex.MultiDexApplication"

这里配置的Application继承自android.support.multidex包中的MultiDexApplication,如果项目中已经有自定义的Application,可以让该Application继承MultiDexApplication,或者在Application的attachBaseContext方法中做如下调用:

protected void attachBaseContext(Context base) {    super.attachBaseContext(base);    MultiDex.install(this);}
  • build工程。经过上面几步后,使用AndroidStudio Build->Build APK菜单构建debug包。
    如果我们直接构建apk,解压apk后会发现里面还是只有一个classes.dex文件,并没有将dex文件分包,这里主要原因是,我们的项目中的方法数没有超过65536个,所以gradle只为我们生成了一个dex包,为了测试dex分包,我们在上面建立的项目中加入几个测试类,如下图所示:

    其中OtherActivity为MainActivity中的按钮点击后跳转的Activity,Test, Test2, Test3, Test4, Test5这5个类,主要是加入了如下的一些方法:

    这里就是添加了很多空方法,让apk中的方法数超过65536个,加入了上面的几个类后,再次使用gradle构建apk,这时候解压生成的apk文件,会发现dex分包终于出来了,如下图所示:

    在Android5.1和Android4.4的机器上测试安装apk并运行,发现能正常运行。
    以上就是使用AndroidStudio进行dex分包的过程,但是有个问题,就是在上面的步骤中,我们没有指定哪些类放在classes.dex包中,完全是由打包工具来确定哪些类放在classes.dex包中的,反编译生成的apk文件后发现,MainActivity和OtherActivity,还有Test, Test两个类放到classes.dex包中了,如下图所示:

    至于如何自定义classes.dex包中的类,这里我还没有找到方法,所以先留个疑问在这里吧。

Eclipse中的Dex分包步骤

Eclipse采用Ant作为项目的构建工具,我们需要自定义Ant脚本,才能实现Dex的多分包,下面还是以一个Demo来说明Dex多分包的步骤:

  • 在Eclipse中新建Android工程Test,这里我们采用如下的配置:

    新建工程后会发现Test工程依赖appcompat_v7包,所以我们的Ant脚本里,还要配置appcompat_v7相关的脚本,Ant的build.xml脚本内容比较多,也比较复杂,需要了解Ant打包apk的过程才能懂build.xml文件的意思,我也是参考了这篇博文才实现了ant dex多分包的,在这里感谢作者,作者的三篇有关Android热修复的文章,给我的帮助非常大。下面贴出我的build.xml文件的内容:
<project    name="multiDex"    default="sign-apk" >    <taskdef        classpath="H:\apache-ant-1.9.7\lib\ant-contrib-1.0b3.jar"        resource="net/sf/antcontrib/antlib.xml" />        <property        name="project-dir"        value="." />        <property        name="appcompat-dir"        value="H:\adt-bundle-windows-x86_64-20140702\workspace\appcompat_v7" />    <target name="init" >                <echo>Init        echo>                <delete dir="${project-dir}\bin" />        <delete dir="${project-dir}\gen" />        <mkdir dir="${project-dir}\bin" />        <mkdir dir="${project-dir}\gen" />        <mkdir dir="${project-dir}\bin/classes" />    target>            <property        name="sdk-folder"        value="H:\adt-bundle-windows-x86_64-20140702\sdk" />        <property        name="platform-tools-folder"        value="${sdk-folder}\build-tools\android-4.4W" />        <property        name="tools.aapt"        value="${platform-tools-folder}\aapt.exe" />    <property        name="platform-folder"        value="${sdk-folder}\platforms\android-20" />        <property        name="android-jar"        value="${platform-folder}\android.jar" />        <property        name="manifest"        value="${project-dir}\AndroidManifest.xml" />        <property        name="appcompat.manifest"        value="${appcompat-dir}\AndroidManifest.xml" />    <target        name="GenR"        depends="init" >                <echo>gen project  R.java         echo>        <exec            executable="${tools.aapt}"            failonerror="true" >            <arg value="package" />            <arg value="-m" />            <arg value="-J" />            <arg value="${project-dir}\gen" />            <arg value="-M" />            <arg value="${manifest}" />            <arg value="-S" />            <arg value="${project-dir}\res" />            <arg value="-S" />            <arg value="${appcompat-dir}\res" />            <arg value="-I" />            <arg value="${android-jar}" />            <arg value="--auto-add-overlay" />        exec>                <echo>gen appcompat  R.java         echo>        <exec            executable="${tools.aapt}"            failonerror="true" >            <arg value="package" />            <arg value="-m" />            <arg value="-J" />            <arg value="${project-dir}\gen" />            <arg value="-M" />            <arg value="${appcompat.manifest}" />            <arg value="-S" />            <arg value="${project-dir}\res" />            <arg value="-S" />            <arg value="${appcompat-dir}\res" />            <arg value="-I" />            <arg value="${android-jar}" />            <arg value="--auto-add-overlay" />        exec>    target>        <target        name="compile"        depends="GenR" >        <echo>        Compiling java source code...        echo>                <javac            bootclasspath="${android-jar}"            destdir="${project-dir}/bin/classes"            encoding="UTF-8" >            <src path="${appcompat-dir}/src" />            <src path="gen" />            <classpath>                <fileset                    dir="${appcompat-dir}/libs"                    includes="*.jar" />                            classpath>        javac>                <javac            bootclasspath="${android-jar}"            destdir="${project-dir}/bin/classes"            encoding="UTF-8" >            <src path="${project-dir}/src" />            <src path="gen" />            <classpath>                <fileset                    dir="${project-dir}/libs"                    includes="*.jar" />                            classpath>            <classpath>                <fileset                    dir="${appcompat-dir}/libs"                    includes="*.jar" />                            classpath>        javac>    target>        <property        name="tools.dx"        value="${platform-tools-folder}\dx.bat" />    <target        name="dex"        depends="compile" >        <echo>            Generate multi-dex...        echo>                <exec            executable="${tools.dx}"            failonerror="true" >            <arg value="--dex" />            <arg value="--multi-dex" />            <arg value="--set-max-idx-number=10000" />            <arg value="--minimal-main-dex" />            <arg value="--main-dex-list" />            <arg value="main-dex-rule.txt" />            <arg value="--output=bin" />            <arg value="bin/classes" />        exec>        <echo>            Generate dex...          echo>        <exec            executable="${tools.dx}"            failonerror="true" >            <arg value="--dex" />            <arg value="--output=bin/classes3.dex" />            <arg value="${project-dir}/libs" />                        <arg value="${appcompat-dir}\libs" />        exec>    target>        <target        name="build-res-and-assets"        depends="dex" >        <echo>    build-res-and-assets         echo>                <exec            executable="${tools.aapt}"            failonerror="true" >            <arg value="package" />            <arg value="-f" />                        <arg value="-M" />                        <arg value="${manifest}" />            <arg value="-S" />                        <arg value="res" />            <arg value="-S" />            <arg value="${appcompat-dir}/res" />            <arg value="-A" />            <arg value="assets" />            <arg value="-I" />            <arg value="${android-jar}" />            <arg value="-F" />                        <arg value="${project-dir}/bin/resources.arsc" />            <arg value="--auto-add-overlay" />        exec>    target>        <target        name="package"        depends="build-res-and-assets" >        <echo>        package unsign          echo>        <java            classname="com.android.sdklib.build.ApkBuilderMain"            classpath="${sdk-folder}/tools/lib/sdklib.jar" >            <arg value="${project-dir}/bin/unsign.apk" />                        <arg value="-u" />                        <arg value="-z" />                        <arg value="${project-dir}/bin/resources.arsc" />            <arg value="-f" />                        <arg value="bin/classes.dex" />        java>    target>        <property        name="jdk-folder"        value="C:\Program Files\Java\jdk1.6.0_43" />        <property        name="tools.jarsigner"        value="${jdk-folder}\bin\jarsigner.exe" />        <target        name="copy_dex"        depends="package" >        <echo message="copy dex..." />        <copy todir="${project-dir}" >            <fileset dir="bin" >                <include name="classes*.dex" />            fileset>        copy>    target>    <target        name="add-subdex-toapk"        depends="copy_dex" >        <echo message="Add Subdex to Apk ..." />        <foreach            param="dir.name"            target="aapt-add-dex" >            <path>                <fileset                    dir="bin"                    includes="classes*.dex" />            path>        foreach>    target>        <target name="aapt-add-dex" >        <echo message="${dir.name}" />                <propertyregex            casesensitive="false"            input="${dir.name}"            property="dexfile"            regexp="classes(.*).dex"            select="\0" />                <if>            <equals                arg1="${dexfile}"                arg2="classes.dex" />            <then>                <echo>            ${dexfile} is not handle                echo>            then>            <else>                <echo>                    ${dexfile} is handle                echo>                <exec                    executable="${tools.aapt}"                    failonerror="true" >                    <arg value="add" />                    <arg value="bin/unsign.apk" />                    <arg value="${dexfile}" />                exec>            else>        if>        <delete file="${project-dir}\${dexfile}" />    target>      <target        name="sign-apk"        depends="add-subdex-toapk" >        <echo>            Sign apk        echo>        <exec            executable="${tools.jarsigner}"            failonerror="true" >            <arg value="-keystore" />            <arg value="${project-dir}/my.keystore" />            <arg value="-storepass" />            <arg value="123456" />                        <arg value="-keypass" />            <arg value="123456" />                        <arg value="-signedjar" />            <arg value="${project-dir}/bin/sign.apk" />            <arg value="${project-dir}/bin/unsign.apk" />            <arg value="test" />                    exec>    target>project>

这里需要注意的一点是:以上配置文件中有关路径的,如JDK路径,AndroidSDK路径,appcompat_v7包的路径等,需要配置你自己的电脑上的路径。以上的build.xml脚本的过程大致如下:

  1. 初始化,主要是删除bin目录和其中的文件,然后配置一些参数。
  2. 使用aapt工具将src目录下的资源文件编译成R.java文件。(由于这里的项目中没有使用aidl,所以这里的ant脚本中没有配置编译aidl文件的过程)
  3. 使用aapt工具将依赖库编译。
  4. 编译java源文件为class文件。
  5. 编译class文件为Dex文件,包括Dex分包。
  6. 将Dex包打成未签名的apk包,然后再签名apk。

以上就是build.xml文件中配置的过程的一个粗略说明。然后需要注意main-dex-rule.txt文件,该文件指定了哪些类放在classes.dex文件中,这里我的main-dex-rule.txt文件内容如下:

com/example/test/BaseApplication.classcom/example/test/MainActivity.class

即将BaseApplication和MainActivity放到主Dex文件中。

  • 编写完build.xml文件后,再编写我们工程中的java代码,这里的代码非常简单,就是一个普通的BaseApplication类,继承自Application,然后两个Activity,从MainActivity跳转到OtherActivity。
    在项目的根目录下运行ant命令,有可能会遇到如下错误:
com.android.dx.cf.iface.ParseException: bad class file magic (cafebabe) or version (0034.0000) 

这是由于电脑上的jdk版本和Eclipse中配置的jdk版本不一致造成的,建议不要用太新的java版本,这里我卸载了电脑上的java8后重新安装了java6将该问题解决。
ant打包成功后,我们可以看到项目的bin目录下,出现了如下几个文件:

可以看到dex分包已经成功生成了,而且在sign.apk文件中,也存在两个dex包。下面将sign.apk安装到手机上,发现在Android5.1系统上可以正常运行,从MainActivity跳转到OtherActivity也没问题,但是在Android4.4的手机上,从MainActivity跳转到OtherActivity时,应用直接崩溃,查看log信息发现错误主要是找不到OtherActivity类,这个问题困扰我好几天,最后发现原来在Android4.4系统上,classes2.dex文件是不会主动加载的,需要我们在代码里手动加载。下面问题来了,怎么在代码中去加载apk中的dex分包呢?

Dex分包的加载

关于dex分包的加载,网上的很多文章里虽然都有介绍,但是要么写得不详细,要么就是代码不全,这里我先是通过将上面生成的classes2.dex和classes3.dex两个dex分包放到手机的sd卡根目录下,然后在BaseApplication中通过如下的代码去加载:

package com.example.test;import java.io.File;import java.lang.reflect.Array;import java.lang.reflect.Field;import android.app.Application;import android.os.Environment;import android.util.Log;import android.widget.Toast;import dalvik.system.DexClassLoader;import dalvik.system.PathClassLoader;public class BaseApplication extends Application {    @Override    public void onCreate() {        super.onCreate();        Toast.makeText(this, "Application init...", Toast.LENGTH_SHORT).show();        String dex2 = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "classes2.dex";        String dex3 = Environment.getExternalStorageDirectory().getAbsolutePath() + File.separator + "classes3.dex";        Log.i("yubo", "dex2 path: " + dex2);        Log.i("yubo", "dex3 path: " + dex3);        inject(dex2);        inject(dex3);    }    public String inject(String libPath) {          boolean hasBaseDexClassLoader = true;          try {              Class.forName("dalvik.system.BaseDexClassLoader");          } catch (ClassNotFoundException e) {              hasBaseDexClassLoader = false;          }          if (hasBaseDexClassLoader) {              PathClassLoader pathClassLoader = (PathClassLoader)getClassLoader();              DexClassLoader dexClassLoader = new DexClassLoader(libPath, getDir("dex", 0).getAbsolutePath(), libPath, getClassLoader());              try {                  Object dexElements = combineArray(getDexElements(getPathList(pathClassLoader)), getDexElements(getPathList(dexClassLoader)));                  Object pathList = getPathList(pathClassLoader);                  setField(pathList, pathList.getClass(), "dexElements", dexElements);                  return "SUCCESS";              } catch (Throwable e) {                  e.printStackTrace();                  return android.util.Log.getStackTraceString(e);              }          }          return "SUCCESS";      }    public Object getPathList(Object baseDexClassLoader)            throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {        return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");    }    public Object getField(Object obj, Class<?> cl, String field)            throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {        Field localField = cl.getDeclaredField(field);        localField.setAccessible(true);        return localField.get(obj);    }    public static void setField(Object obj, Class<?> cl, String field, Object value)            throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {        Field localField = cl.getDeclaredField(field);        localField.setAccessible(true);        localField.set(obj, value);    }    public Object getDexElements(Object paramObject)            throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException {        return getField(paramObject, paramObject.getClass(), "dexElements");    }    public static Object combineArray(Object arrayLhs, Object arrayRhs) {        Class<?> localClass = arrayLhs.getClass().getComponentType();        int i = Array.getLength(arrayLhs);        int j = i + Array.getLength(arrayRhs);        Object result = Array.newInstance(localClass, j);        for (int k = 0; k < j; ++k) {            if (k < i) {                Array.set(result, k, Array.get(arrayLhs, k));            } else {                Array.set(result, k, Array.get(arrayRhs, k - i));            }        }        return result;    }}

在BaseApplication中加入以上代码后,重新用ant打包apk,安装到Android4.4的手机上,发现终于成功从MainActivity跳转到OtherActivity了,这说明我们放到sd卡的dex文件得到了加载,但是这里仅仅是从sd卡加载dex文件,我们的Android应用打包成apk后,dex文件都放在apk包中,这里就出现新问题了,怎么在代码中加载apk包里的dex分包呢?
在AndroidStudio项目中,我们使用了android.support.multidex包来加载dex分包,通过查看android.support.multidex包的源码,我发现在源码中就有从apk文件里读取dex文件的代码,主要是下面的部分:

下面贴上我的代码,这段代码主要是从已安装的apk中获取dex文件,并将这些dex文件拷贝到应用的私有目录中,当app启动时,从私有目录中读取并加载这些dex文件,下面是BaseApplication的完整代码:

package com.example.test;import java.io.File;import java.io.FileInputStream;import java.io.FileOutputStream;import java.io.InputStream;import java.lang.reflect.Array;import java.lang.reflect.Field;import java.util.Enumeration;import java.util.zip.ZipEntry;import java.util.zip.ZipFile;import android.app.Application;import android.util.Log;import android.widget.Toast;import dalvik.system.DexClassLoader;import dalvik.system.PathClassLoader;public class BaseApplication extends Application {    @Override    public void onCreate() {        super.onCreate();        Toast.makeText(this, "Application init...", Toast.LENGTH_SHORT).show();        try {            loadDex();            injectDex();        } catch (Exception e) {            e.printStackTrace();        }    }    //加载dex分包    private void loadDex() throws Exception {        File originFile = new File(getApplicationInfo().sourceDir); //需要拷贝的apk文件        File newAPK = new File(getFilesDir().getAbsolutePath() + File.separator + "app.apk"); // 获取拷贝后的apk存放路径        if(!newAPK.exists()) {            newAPK.createNewFile();        }        //拷贝apk到私有目录        copyFile(new FileInputStream(originFile), new FileOutputStream(newAPK));        //解压apk中的dex文件        ZipFile apk = new ZipFile(newAPK);        Enumeration<? extends ZipEntry> en = apk.entries();        ZipEntry zipEntry = null;        while(en.hasMoreElements()) {            zipEntry = (ZipEntry) en.nextElement();            if(!zipEntry.isDirectory() && zipEntry.getName().endsWith("dex") && !"classes.dex".equals(zipEntry.getName())) {                //拷贝dex到destDir                Log.e("yubo", "zip entry name: " + zipEntry.getName() + ", file size: " + zipEntry.getSize());                InputStream inputStream = apk.getInputStream(zipEntry);                FileOutputStream fout = openFileOutput(zipEntry.getName(), MODE_PRIVATE);                copyFile(inputStream, fout);            }        }        apk.close();    }    //从app_dex目录下取出dex文件并注入到系统的PathClassLoader    private void injectDex() {        File[] files = getFilesDir().listFiles();        if(files != null) {            for(File f : files) {                String fileName = f.getName();                if(fileName.endsWith("dex") && !"classes.dex".equals(fileName)) {                    inject(f.getAbsolutePath());                    Log.e("yubo", "inject file: " + f.getAbsolutePath());                }            }        }    }    //拷贝文件    private void copyFile(InputStream is, FileOutputStream fos) {        try {            int hasRead = 0;            byte[] buf = new byte[1024];            while((hasRead = is.read(buf)) > 0) {                fos.write(buf, 0, hasRead);            }            fos.flush();        } catch (Exception e) {            e.printStackTrace();        } finally {            try {                if (fos != null) {                    fos.close();                }                if(is != null) {                    is.close();                }            } catch (Exception e2) {            }        }    }    //注入dex包,libPath为dex包的路径    public String inject(String libPath) {          boolean hasBaseDexClassLoader = true;          try {              Class.forName("dalvik.system.BaseDexClassLoader");          } catch (ClassNotFoundException e) {              hasBaseDexClassLoader = false;          }          if (hasBaseDexClassLoader) {              PathClassLoader pathClassLoader = (PathClassLoader)getClassLoader();              DexClassLoader dexClassLoader = new DexClassLoader(libPath, getDir("dex", 0).getAbsolutePath(), libPath, getClassLoader());              try {                  Object dexElements = combineArray(getDexElements(getPathList(pathClassLoader)), getDexElements(getPathList(dexClassLoader)));                  Object pathList = getPathList(pathClassLoader);                  setField(pathList, pathList.getClass(), "dexElements", dexElements);                  return "SUCCESS";              } catch (Throwable e) {                  e.printStackTrace();                  return android.util.Log.getStackTraceString(e);              }          }          return "SUCCESS";      }    public Object getPathList(Object baseDexClassLoader)            throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {        return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");    }    public Object getField(Object obj, Class<?> cl, String field)            throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {        Field localField = cl.getDeclaredField(field);        localField.setAccessible(true);        return localField.get(obj);    }    public static void setField(Object obj, Class<?> cl, String field, Object value)            throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {        Field localField = cl.getDeclaredField(field);        localField.setAccessible(true);        localField.set(obj, value);    }    public Object getDexElements(Object paramObject)            throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException {        return getField(paramObject, paramObject.getClass(), "dexElements");    }    public static Object combineArray(Object arrayLhs, Object arrayRhs) {        Class<?> localClass = arrayLhs.getClass().getComponentType();        int i = Array.getLength(arrayLhs);        int j = i + Array.getLength(arrayRhs);        Object result = Array.newInstance(localClass, j);        for (int k = 0; k < j; ++k) {            if (k < i) {                Array.set(result, k, Array.get(arrayLhs, k));            } else {                Array.set(result, k, Array.get(arrayRhs, k - i));            }        }        return result;    }}

拷贝dex文件的代码主要是loadDex()方法和injectDex()方法,这里需要注意的是,getApplicationInfo().sourceDir代表应用的apk目录,当一个应用被安装到手机上后,会在getApplicationInfo().sourceDir生成一个和被安装的apk一样的apk,我们要拷贝的也是这个apk,拷贝好apk到应用私有目录后,再解压出其中的dex文件,由于我们使用了openFileOutput(),所以dex文件的存放路径在data/data//files目录下,通过getFileDir()也能得到这个目录的路径。
经过以上加载dex的过程,再使用ant打包后安装运行apk,发现从MainActivity跳转到OtherActivity不会报错了~

参考文章:

  1. Android 热修复三部曲之基本的Ant打包脚本
  2. Android热修复三部曲之MultiDex 分包架构
  3. Android热修复三部曲之动态加载补丁.dex文件
  4. dex分包变形记
  5. Android dex分包方案

本文的demo代码已上传到我的github:
Eclipse项目
AndroidStudio项目

更多相关文章

  1. 没有一行代码,「2020 新冠肺炎记忆」这个项目却登上了 GitHub 中
  2. 一款常用的 Squid 日志分析工具
  3. GitHub 标星 8K+!一款开源替代 ls 的工具你值得拥有!
  4. RHEL 6 下 DHCP+TFTP+FTP+PXE+Kickstart 实现无人值守安装
  5. Linux 环境下实战 Rsync 备份工具及配置 rsync+inotify 实时同步
  6. android实现权限管理和签名静默卸载
  7. Android目录结构(详解)
  8. 什么特性造就了Android快速启动
  9. Android中使用kotlin实现多行文本的上下滚动播放

随机推荐

  1. android平台下基于MediaRecorder和AudioR
  2. Android星星评分控件RatingBar的使用
  3. Android studio 添加assets文件夹
  4. android 获取屏幕高度,宽度,状态栏高度
  5. 搭建Android(安卓)CTS测试环境总结
  6. Android ROM 开发技能图谱
  7. (译)Android 性能优化总览
  8. Android仿QQ主界面-------完善篇
  9. android 学习八 android selector的使用
  10. Android自己动手实现下拉刷新控件(1)----典