自己动手生成 Android Apk

3,130 阅读4分钟
原文链接: blog.csdn.net

  本文仅做学习使用,比较简单,没有实际项目复杂。
  掌握Apk生成的过程,对于我们是非常重要的。而该过程平时都是由IDE自动帮我们完成的。IDE可以给我们带来很大的便利,但是也是一把双刃剑,有时候会让我们忽视一些重要的实现步骤。所以我们在使用IDE自动编译打包Apk时,也应该知道Apk是怎么一步步由我们项目中的java文件,资源文件等变成我们所熟知的Apk的。不管使用什么IDE,Android 打包生成Apk主要都是由以下几步完成:
  1.根据资源文件和AndroidManifest.xml生成R.java文件
  2.处理aidl,生成对应的java文件,如果没有aidl,则跳过
  3.编译工程源码(主项目,库)src目录下所有的源码,同时上边生成的R.java和aidl生成的java文件也会被编译生成相应的class文件
  4.将第3步生成的class文件打包生成.dex文件
  5.将资源文件打包,生成初始的apk
  6.将第4步生成的.dex文件加入到apk中生成未签名的包
  7.apk签名
  以上即为主要的步骤。盗用一张官方经典的打包流程图
  
            

  如果看懂了这张图,那么基本上就算理解了Apk的打包过程。这里需要用到好几个sdk提供的工具,为了方便可以将sdk/build-tools路径添加到环境变量中。为了能够清楚的看到每一步我们生成了结果,可以在项目app/src/main下建几个目录:

$ mkdir -p app/src/main/{gen,build,out}

  接下来我们就先看第一步生成R.java文件。

  0x01 生成R.java

  R.java对于Android开发同学,肯定非常熟悉,它是一个非常重要的一个类,并且它是自动生成的,主要是包含了res目录下所有资源的ID,我们可以在程序中通过它来获取资源ID,然后通过Android Api来获取具体的资源。
  

 TextView tv = (TextView) findViewById(R.id.tv_text);
        tv.setText(R.string.app_name);

  R.java是由SDK中提供的aapt(sdk/build-tools/ 中对应的版本下,本文使用的25.0.0,那么具体的路径就是sdk/build-tools/25.0.0)工具生成的。aapt是非常重要的工具,它不仅可以生成R.java,还能将res下除assets目录外其他的目录下普通的xml文件编译成二进制xml文件,并将这些文件打包成初始包。如果了解过插件化,相比都知道插件化有一个非常重要的问题就是如何保证宿主程序和插件程序的资源ID不会冲突,其中有一种思路就是通过修改aapt源码来修改资源ID的PP段来保证资源ID不冲突。具体可以参考插件化开源项目 ACDD 或者携程的DynamicAPK。关于aapt源码分析可以参考罗升阳老师的文章blog.csdn.net/luoshengyan…。这里只讲解使用aapt工具生成R.java。
  在Android Studio中切换到terminal(如果使用终端也是可以的,可以将当前路径切换到实验项目的根路径下)中。输入以下命令:
  

aapt package -f 
-M ./app/src/main/AndroidManifest.xml 
-I ~/Library/Sdk/platforms/android-25/android.jar 
-S ./app/src/main/res/ 
-J ./app/src/main/gen/ 
-m

  参数的含义如下:
  -f : #如果编译出来的文件已经存在,强制覆盖
  -M : AndroidManifest.xml #AndroidManifest.xml 的路径
  -I : xx/android.jar #某个版本平台的android.jar 的路径,具体是在你放sdk目录的sdk/platforms/android-XX/android.jar
  -S : res/ #资源文件res 文件夹路径
  -J :gen/ #生成 R.java 的输出目录
  -m : #使得生成的包的目录放在 -J 参数指定的目录
  如果提示没有aapt命令,请检查一下aapt环境变量,或者直接通过绝对路径引用。如果没有问题,那么在gen文件下会生成相应的R.java文件。

      
    
  关于R.java在合并资源和掌握Android资源管理非常重要,应该好好的掌握它的结构。可以自行参考上边推荐的文档,这里不做详细介绍。我们接着看一下aidl的编译。

  0x02 编译aidl文件
  
  我们在使用aidl时一般是先创建一个xxxInterface.aidl文件,然后IDE会自动的帮我们生成一个xxxInterface.java文件,其实IDE也是通过sdk提供的aidl工具实现的。为了方便演示,我在main目录下创建一个aidl文件夹,里边新建一个IMyAidlInterface.aidl文件。然后就可以使用aidl工具来编译了,该工具同aapt路径一样。使用如下命令进行编译:
  

 $aidl -Iapp/src app/src/main/aidl/com/chuck/manualapk/aidl/IMyAidlInterface.aidl 

  注意这里的-I和”app/src”中间没有空格,否则无法成功运行,[无奈],可以参考How do I use AIDL tool from command line using SDK sample code?。成功后会在.aidl目录下生成java文件,当然你也可以通过-o指定java文件的生成路径。
  
      

  如果没有aidl文件可以跳过这一步。

  0x03 javac 编译所有java文件
  
  这一步其实就是使用jdk的javac将项目中所有的java文件编译成class文件。
  

 javac -source 1.7 -target 1.7 -encoding UTF-8 -bootclasspath ~/Library/Sdk/platforms/android-25/android.jar -d app/src/main/build/ app/src/main/java/com/chuck/manualapk/*.java app/src/main/gen/com/chuck/manualapk/*.java app/src/main/aidl/com/chuck/manualapk/aidl/*.java

   -source 1.7 -target 1.7:指定编译的jdk版本,如果不指定的话,会报错。
  -encoding :编码方式,这里设置UTF-8
  -bootclasspath :引导类文件的路径,这里需要使用到android.jar中的Android API。
  -d :生成的class文件存放路径
       

  接下来需要把生成的class文件打包生成dex文件。

  0x04 打包生成.dex文件

  dex文件是Android虚拟机可运行文件,可以使用dx工具生成。具体如下:
  

 $dx --dex --output=app/src/main/build/classes.dex app/src/main/build/

  dx工具会将上一步app/src/main/build/中生成的class文件全部打包,得到classes.dex,apk包中包含的classes.dex就是这样生成的,当然实际项目可能类会很多,但是过程是一样的。
        

  有了classes.dex,开始要打包apk了,我们先将资源文件做成初始包

  0x05 打包资源文件

  apk包中的资源文件都是经过编译过的,是二进制文件。我们还是使用aapt工具:
  

$ aapt package -f -M app/src/main/AndroidManifest.xml -I ~/Library/Sdk/platforms/android-25/android.jar -S app/src/main/res/ -F app/src/main/out/res.apk

  将AndroidManifest.xml与res目录除了assets目录以为的文件进行编译,生成resource.arsc和若干二进制的xml文件。资源ID和资源文件的对应关系都是在resource.arsc中的。这些文件都会被打包到res.apk中。

      
     
  如图,将res.apk后缀改为.zip然后解压,可以得到刚刚提到的文件。
  接下来的任务就是将classes.dex文件打进apk中,这样就可以得到没有签名的apk。

  0x06 将.dex将包到apk

  以前可以直接使用apkbuilder工具直接打包,但是目前sdk将该工具移除了,不过没有关系,我们可以自己生成一个,也可以直接使用apkbuilder的实现类,该类是sdk/tools/lib/sdklib.jar文件中的com.android.sdklib.build.ApkBuilderMain。为了方便我们可以生成一个apkbuilder,将当前目录切换到sdk的tools目录下,执行以下命令:
  

$ cat android | sed -e 's/com.android.sdkmanager.Main/com.android.sdklib.build.ApkBuilderMain/g' > apkbuilder

  此时在tools目录就会出现apkbuilder了,如果没有执行权限,可以加上执行权限:
  

$ chmod +x apkbuilder

  这样就可以使用apkbuilder工具了。
  

 ~/Library/Sdk/tools/apkbuilder app/src/main/out/app.apk -v -u -z app/src/main/out/res.apk -f app/src/main/build/classes.dex 

  如果没有出错,会在out目录下得到app.apk,解压后可以看到classes文件以及被打进去了。
        

  到此,已经得到了未签名的apk,剩下的就是apk签名了。

  0x07 签名apk

  apk用安装必须要签名,我们使用jarsigner给apk手动签名,为了方便可以使用debug签名。
  

 $ jarsigner -verbose -keystore ~/.android/debug.keystore -storepass android -keypass android app/src/main/out/app.apk androiddebugkey

  其实我们在IDE签名时,也需要设置storepassword,keypassword,alias别名。如果不出意外apk完成debug签名,就可以进行安装了。
        

  进行完签名,正常来说还是需要进行对齐优化的,能加快apk解压速度,可以使用zipalign工具优化,这里不再做详细介绍,有兴趣的可以尝试一下。
  到此我们就大致的收到的实现了apk的打包过程,还是比较清晰的,现在在回去看,那个图,应该就可以理解了。当然在实际项目中,要比上边的演示流程复杂的多,包括多module,NDK都没有讨论到,但是基本的思路是一样的。下图是更加全面的流程图:

        

  看清来很复杂,如果对主要流程都清楚的话,其实很容易看懂的。最后可以将上边的操作写在脚本(可以是你熟悉的脚本)里边,我是使用的Makefile。
  

# 系统 MacOS 10.12.3
# JDK 1.7
# minSdkVersion 15
# targetSdkVersion 25
# compileSdkVersion 25

#sdk路径
Sdk=~/Library/Sdk
Platforms=$(Sdk)/platforms/android-25
#android.jar路径
android_jar=$(Platforms)/android.jar
#build-tools版本路径
Build_Toots=$(Sdk)/build-tools/25.0.0
#aapt路径
aapt=$(Build_Toots)/aapt
#dx路径
dx=$(Build_Toots)/dx
#aidl路径
aidl=$(Build_Toots)/aidl
apkbuilder=$(Sdk)/tools/apkbuilder
Project_Path=.
Main_Path=$(Project_Path)/app/src/main

#生成R.java
aapt_generate_r:
    $(aapt) package -f \
    -M $(Main_Path)/AndroidManifest.xml \
    -m --auto-add-overlay \
    -I $(android_jar) \
    -S $(Main_Path)/res/ \
    -J $(Main_Path)/gen/ \
    -m

#编译java文件,生成class文件
#需要将所有包含java目录都添加进去
javac_generate_class:
    javac -source 1.7 -target 1.7 \
    -encoding UTF-8 \
    -bootclasspath $(android_jar) \
    -d $(Main_Path)/build \
    ${Main_Path}/java/com/chuck/manualapk/*.java \
    ${Main_Path}/gen/com/chuck/manualapk/*.java \

#生成classes.dex
#将上步中build目录中的class文件,打包生成.dex
dx_generate_classes_dex:
    $(dx) --dex --output=$(Main_Path)/build/classes.dex $(Main_Path)/build/

#使用aapt将资源文件打包
package_res:
    $(aapt) package -f \
    -M $(Main_Path)/AndroidManifest.xml \
    -I $(android_jar) \
    -S $(Main_Path)/res/ \
    -F $(Main_Path)/out/res.apk

#将classes.dex放入apk文件中
package_dex:
    $(apkbuilder) $(Main_Path)/out/app.apk \
    -v -u \
    -z $(Main_Path)/out/res.apk \
    -f $(Main_Path)/build/classes.dex

#签名,使用的debug包
sign_debug:
    jarsigner -verbose \
    -keystore ~/.android/debug.keystore \
    -storepass android \
    -keypass android \
    $(Main_Path)/out/app.apk androiddebugkey


build_apk:
    make aapt_generate_r
    make javac_generate_class
    make dx_generate_classes_dex
    make package_res
    make package_dex
    make sign_debug

  将Makefile放到工程的root目录下,然后修改各工具相应的路径。在terminal中运行make build_apk即可。