Posts 编译你的第一个安卓APP(使用命令行,而不是Android Studio)
Post
Cancel

编译你的第一个安卓APP(使用命令行,而不是Android Studio)

笔者环境:Linux
参考资料:安卓官网

一、下载工具,准备开发环境

(先安装好java开发环境,比如open-jdk) 到 https://developer.android.google.cn/studio 下载command line tools:

在任意位置新建一个文件夹,任意名称,我命名为android_sdk,把下载的commandlinetools解压到这里。
查看usage: tools/bin/sdkmanager --help
列出可安装的内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
[whqee@whqee android_sdk]$ tools/bin/sdkmanager --sdk_root=. --list
Installed packages:=====================] 100% Computing updates...             
  Path    | Version | Description         | Location
  ------- | ------- | -------             | ------- 
  tools   | 1.0.0   | Android SDK Tools 1 | tools/  

Available Packages:                                                       
  中间省略...                                               
Available Updates:
  ID      | Installed | Available
  ------- | -------   | -------  

[whqee@whqee android_sdk]$ 

可以看到一大页的列表,为了方便输入命令,起个别名:
alias sdkmanager="tools/bin/sdkmanager --sdk_root=. "

安装android29平台和工具链,当然你也可以安装其它的:
sdkmanager "platforms;android-29" "build-tools;29.0.3"

文件夹现状:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[whqee@whqee android_sdk]$ tree -L 2
.
├── build-tools
│   └── 29.0.3
├── licenses
│   └── android-sdk-license
├── platforms
│   └── android-29
└── tools
    ├── bin
    ├── lib
    ├── NOTICE.txt
    ├── package.xml
    └── source.properties

8 directories, 4 files

二、开始构建第一个安卓APP

1、构建前的准备

在任意地方新建一个文件夹,比如我新建一个名为“hello_apk”的文件夹,进入文件夹。
新建一个res文件夹(resource),里面分别新建layout(UI布局)和values(变量)文件夹,并新建两个文件,如下:

1
2
3
4
5
6
7
8
[whqee@whqee hello_apk]$ tree res
res
├── layout
│   └── helloUI.xml
└── values
    └── strings.xml

2 directories, 2 files

其中,helloUI.xml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"
   android:layout_width="match_parent"
   android:layout_height="match_parent" >
   
   <TextView
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_centerHorizontal="true"
      android:layout_centerVertical="true"
      android:text="@string/hello_msg"
      tools:context=".hello" />

  <Button
          android:id="@+id/startbutton"
          android:layout_width="wrap_content"
          android:layout_height="wrap_content"
          android:layout_below="@+id/time_display_box"
          android:layout_centerHorizontal="true"
          android:layout_marginTop="41dp"
          android:text="@string/start" />

</RelativeLayout>

strings.xml:

1
2
3
4
5
6
7
<resources>
   <string name="app_name">APP Hello Android</string>
   <string name="hello_msg">Hello Android!</string>
   <string name="menu_settings">Settings</string>
   <string name="title_activity_main">MainActivity</string>
   <string name="start">Start</string>
</resources>

根目录新建一个hello_android_manifest.xml文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
<?xml version='1.0'?>
<manifest xmlns:a='http://schemas.android.com/apk/res/android' package='demo.demo' a:versionCode='1' a:versionName='1'>
    <application a:label='A Hello Android'>
        <activity a:name='demo.demo.hello'>
             <intent-filter>
                <category a:name='android.intent.category.LAUNCHER'/>
                <action a:name='android.intent.action.MAIN'/>
             </intent-filter>
        </activity>
    </application>
</manifest>

新建java/demo文件夹路径(省得编译后的临时.class文件在根目录显得乱),里面新建一个hello.java文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
package demo.demo

import android.app.Activity;
import android.os.Bundle;
import android.widget.Button;
import android.widget.EditText;
import android.view.View;

public class hello extends Activity {
   @Override
   protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      setContentView(R.layout.helloUI);

	  Button startButton = (Button) findViewById(R.id.startbutton);
	  startButton.setOnClickListener(new View.OnClickListener() {
		  public void onClick(View view) {
			  try {

			  } catch (NumberFormatException e) {

			  }
		  }
	  });
   }
}

然后所有文件路径看起来是这样的了:

1
2
3
4
5
6
7
8
9
10
11
12
13
[whqee@whqee hello_apk]$ tree
.
├── hello_android_manifest.xml
├── java
│   └── demo
│       └── hello.java
└── res
    ├── layout
    │   └── helloUI.xml
    └── values
        └── strings.xml

5 directories, 4 files

2、开始构建APK

导出build-tools的路径到全局环境变量:
export PATH=~/Development/Android/android_sdk/build-tools/29.0.3/:$PATH

声明路径变量,方便引用android.jar:
PLATFORM=~/Development/Android/android_sdk/platforms/android-29/android.jar

新建build文件夹,
(1)先把res里的资源编译成.flat文件:

1
aapt2 compile res/*/* -o build/

(2)把.flat文件链接成.apk,并生成R.java:

1
aapt2 link -o unsigned.demo.apk -I $PLATFORM build/* --manifest hello_android_manifest.xml --java build`  

(3)编译java文件成.class(现状只有R.java和hello.java):

1
javac `find . -name "*.java"` -classpath $PLATFORM

(4)使用d8把.class编译成一个.dex

1
d8 --lib $PLATFORM --release --output build/ build/demo/demo/R*.class

(5)前面打包的apk里只有UI、变量等资源,现在把程序逻辑打包进去:

1
aapt add unsigned.demo.apk build/classes.dex

(6)将.apk后缀的zip压缩包内容按4字节对齐(非必要,减少运行后RAM占用量):

1
zipalign -p 4 unsigned.demo.apk unsigned.aligned.demo.apk

(7)使用keytool生成证书/导入证书/生成密钥/导入密钥,查看帮助:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[whqee@whqee hello_apk]$ keytool --help
密钥和证书管理工具

命令:

 -certreq            生成证书请求
 -changealias        更改条目的别名
 -delete             删除条目
 -exportcert         导出证书
 -genkeypair         生成密钥对
 -genseckey          生成密钥
 -gencert            根据证书请求生成证书
 -importcert         导入证书或证书链
 -importpass         导入口令
 -importkeystore     从其他密钥库导入一个或所有条目
 -keypasswd          更改条目的密钥口令
 -list               列出密钥库中的条目
 -printcert          打印证书内容
 -printcertreq       打印证书请求的内容
 -printcrl           打印 CRL 文件的内容
 -storepasswd        更改密钥库的存储口令

使用 "keytool -command_name -help" 获取 command_name 的用法
[whqee@whqee hello_apk]$ 

生成一个密钥对,alias不填默认为“mykey”:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[whqee@whqee hello_apk]$ keytool -genkeypair -validity 10000 -keystore hello.keystore -keyalg RSA -alias whqee.cn -storetype pkcs12
输入密钥库口令:  
再次输入新口令: 
您的名字与姓氏是什么?
  [Unknown]:  whqee.cn
您的组织单位名称是什么?
  [Unknown]:  whqee.cn
您的组织名称是什么?
  [Unknown]:  whqee.cn
您所在的城市或区域名称是什么?
  [Unknown]:  
您所在的省/市/自治区名称是什么?
  [Unknown]:  
该单位的双字母国家/地区代码是什么?
  [Unknown]:  
CN=whqee.cn, OU=whqee.cn, O=whqee.cn, L=Unknown, ST=Unknown, C=Unknown是否正确?
  [否]:  y

[whqee@whqee hello_apk]$ 

查看这个密钥对(公钥+私钥):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
[whqee@whqee hello_apk]$ keytool -list -v -keystore hello.keystore
输入密钥库口令:  
密钥库类型: PKCS12
密钥库提供方: SUN

您的密钥库包含 1 个条目

别名: whqee.cn
创建日期: 2020-4-10
条目类型: PrivateKeyEntry
证书链长度: 1
证书[1]:
所有者: CN=whqee.cn, OU=whqee.cn, O=whqee.cn, L=Unknown, ST=Unknown, C=Unknown
发布者: CN=whqee.cn, OU=whqee.cn, O=whqee.cn, L=Unknown, ST=Unknown, C=Unknown
序列号: 4b7bc328
有效期为 Fri Apr 10 02:29:40 CST 2020 至 Tue Aug 27 02:29:40 CST 2047
证书指纹:
         MD5:  CD:C8:74:B0:3A:CC:43:56:B0:72:F9:15:F5:3C:06:28
         SHA1: 55:E2:7A:BB:65:53:BD:C7:09:E4:D1:59:22:3F:E9:16:A1:2D:CF:11
         SHA256: EB:A8:A4:DE:CD:E5:E0:9D:00:55:09:6E:4B:F1:8B:E5:28:8E:EF:3A:07:13:81:6B:FD:BA:3F:78:F2:29:3D:AF
签名算法名称: SHA256withRSA
主体公共密钥算法: 2048 位 RSA 密钥
版本: 3

扩展: 
(省略...)

导出密钥证书(只包含公钥):

1
2
3
4
[whqee@whqee hello_apk]$ keytool -exportcert -keystore hello.keystore -alias whqee.cn -file hello.crt
输入密钥库口令:  
存储在文件 <hello.crt> 中的证书
[whqee@whqee hello_apk]$ 

(8)使用密钥签名apk:

1
2
3
[whqee@whqee hello_apk]$ apksigner sign --ks hello.keystore --out hello-release.apk unsigned.aligned.demo.apk 
Keystore password for signer #1: 
[whqee@whqee hello_apk]$ 

完成。

附合集:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash
export PATH=~/Development/Android/android_sdk/build-tools/26.0.3/:$PATH
PLATFORM="/opt/android-sdk/platforms/android-29/android.jar"
rm -r build
rm my-release-key.jks demo-release.apk unsigned.aligned.demo.apk unsigned.demo.apk
mkdir -p build
aapt2 compile res/*/* -o build/
aapt2 link -o unsigned.demo.apk -I $PLATFORM build/* --manifest hello_android_manifest.xml --java build
javac `find . -name "*.java"` -classpath $PLATFORM 
d8 --lib $PLATFORM --release --output build/ build/demo/demo/R*.class 
aapt add unsigned.demo.apk build/classes.dex 
zipalign -p 4 unsigned.demo.apk unsigned.aligned.demo.apk
keytool -genkey -v -keystore my-release-key.jks -keyalg RSA -validity 10000 -alias whqee.cn -storetype pkcs12
keytool -list -v -keystore my-release-key.jks 
apksigner sign --ks my-release-key.jks --out demo-release.apk unsigned.aligned.demo.apk

另一种方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#!/bin/bash
export PATH=~/Development/Android/android_sdk/build-tools/26.0.3/:$PATH 
set -e

AAPT="~/Development/Android/android_sdk/build-tools/26.0.3/aapt"
DX="~/Development/Android/android_sdk/build-tools/26.0.3/dx"
ZIPALIGN="~/Development/Android/android_sdk/build-tools/26.0.3/zipalign"
APKSIGNER="~/Development/Android/android_sdk/build-tools/26.0.3/apksigner"
PLATFORM="/home/whqee/Development/Android/android_sdk/platforms/android-26/android.jar"
 
echo "Cleaning..."
rm -rf build
# rm my-release-key.jks *.apk 

echo "Generating R.java file..."
mkdir -p build
aapt p -f -m -J build -M AndroidManifest.xml -S res -I $PLATFORM

echo "Compiling..."
javac -d build -classpath build/demo/demo -bootclasspath $PLATFORM -source 1.7 -target 1.7 java/demo/hello.java
javac -d build -classpath build/demo/demo -bootclasspath $PLATFORM -source 1.7 -target 1.7 build/demo/demo/R.java
 
echo "Translating in Dalvik bytecode..."
dx --dex --output=classes.dex build
 
echo "Making APK..."
aapt package -f -m -F build/hello.unaligned.apk -M AndroidManifest.xml -S res -I $PLATFORM
aapt add build/hello.unaligned.apk build/classes.dex
 
echo "Aligning and signing APK..."
zipalign -f 4 build/hello.unaligned.apk build/hello.apk
# keytool -genkeypair -validity 365 -keystore mykey.keystore -keyalg RSA -keysize 2048
keytool -genkey -v -keystore my-release-key.jks -keyalg RSA -keysize 2048 -validity 10000 -alias my-alias
apksigner sign --ks mykey.keystore build/hello.apk
This post is licensed under CC BY 4.0 by the author.