4月的某天,接到了一单批量生成安卓apk包的私活。要求是点开后通过展示不同的背景图片,展示主角的极强业务能力。

由于之前也没写过原生安卓,顶多用React Native写跨端应用。手工创建一堆工程,再手动修改,就完全是体力工作,这个肯定是不行的。

LLM先行

最近一直用大模型解决重复性劳动,直接问下大模型这玩意怎么写。

1
假如你是安卓和Bash脚本开发专家,我需要通过Bash命令生成不定数量的安卓安装包,每个安卓安装包都有不同的图标和图片,请写出Bash脚本。

虽然GPT4写了一堆Bash脚本内容,但在细枝末节还是报错,就把报错信息提供给LLM,让其解决。

但也需要注意,并不是对话越长越好,有时候报错原文和代码一长,就会出现严重的幻觉。因此,写一点、运行一点、验证一点就清理上下文重新提问。

一起学习安卓和Bash

最后LLM和人工的不懈调试下(也就2个小时),输出了可运行Bash脚本:

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
#!/bin/bash

PROJECTS_COUNT=1

for i in $(seq 1 $PROJECTS_COUNT); do
PROJECT_NAME="Dual_Record_$(openssl rand -hex 3)"
PACKAGE_NAME="com.example.$PROJECT_NAME"

# 创建基础的项目结构
mkdir -p "./$PROJECT_NAME/app/src/main/java/$PACKAGE_NAME"
mkdir -p "./$PROJECT_NAME/app/src/main/res/layout"
mkdir -p "./$PROJECT_NAME/app/src/main/res/mipmap"
echo "rootProject.name='$PROJECT_NAME'" > "./$PROJECT_NAME/settings.gradle"
echo "include ':app'" > "./$PROJECT_NAME/settings.gradle"

# 复制图标到项目中
cp /Users/project256/Downloads/5a1acc930a4f210f5ee9e73e45bec3c7_1.png "./$PROJECT_NAME/app/src/main/res/mipmap/ic_launcher.png"
cp /Users/project256/Downloads/5a1acc930a4f210f5ee9e73e45bec3c7_1.png "./$PROJECT_NAME/app/src/main/res/mipmap/ic_launcher_round.png"

# 创建strings.xml文件并添加app_name字符串资源
mkdir -p "./$PROJECT_NAME/app/src/main/res/values/"
cat > "./$PROJECT_NAME/app/src/main/res/values/strings.xml" <<EOF
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">APP名字</string>
</resources>
EOF

# Inside the loop, setting PACKAGE_NAME and PROJECT_NAME

# This goes in the project-level build.gradle creation step
cat > "./$PROJECT_NAME/build.gradle" <<EOF
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
// Make sure to use a compatible version
classpath 'com.android.tools.build:gradle:7.4.2'
}
}

allprojects {
repositories {
google()
mavenCentral()
}
}

task clean(type: Delete) {
delete rootProject.buildDir
}
EOF

# Adjusted AndroidManifest.xml creation with android:exported="true"
cat > "./$PROJECT_NAME/app/src/main/AndroidManifest.xml" <<EOF
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="$PACKAGE_NAME">

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
<activity android:name=".MainActivity"
android:exported="true"> <!-- Ensure this line is added -->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>
EOF

# 创建一个简单的MainActivity
cat > "./$PROJECT_NAME/app/src/main/java/$PACKAGE_NAME/MainActivity.java" <<EOF
package $PACKAGE_NAME;

import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
EOF

# 创建一个简单的activity_main.xml布局文件
cat > "./$PROJECT_NAME/app/src/main/res/layout/activity_main.xml" <<EOF
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="欢迎使用XX系统_$PROJECT_NAME!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
EOF

# 创建app模块的build.gradle文件
cat > "./$PROJECT_NAME/app/build.gradle" <<EOF
apply plugin: 'com.android.application'

android {
compileSdkVersion 31
defaultConfig {
applicationId "$PACKAGE_NAME"
minSdkVersion 21
targetSdkVersion 31
versionCode 1
versionName "1.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}

dependencies {
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
EOF

# 初始化Gradle Wrapper
cd "$PROJECT_NAME"
gradle wrapper --gradle-version=7.5
cd ..

# 构建项目
cd "$PROJECT_NAME"
./gradlew assembleDebug
cd ..

# 复制APK到脚本执行目录
cp ./"$PROJECT_NAME"/app/build/outputs/apk/debug/app-debug.apk ./"$PROJECT_NAME.apk"

echo "Project $PROJECT_NAME has been created and packaged."
done

echo "Finished creating and packaging $PROJECTS_COUNT projects."

其实很久不用Bash写脚本了,借着这个项目来学习学习~

1
#!/bin/bash

第一行,指定了bash执行器位置,一般./xxx.sh的时候会读取第一行。

1
PROJECTS_COUNT=1

第二行,设定了一个变量,用来指定生成项目数量。

1
2
3
for i in $(seq 1 $PROJECTS_COUNT); do
…………
done

接下来写了一个for语句,seq 1 $PROJECTS_COUNT则生成从1到PROJECTS_COUNT的序列,前方的$符号则将括号内的表达式打包成一个变量,最后在do和done之间写具体逻辑。

1
2
PROJECT_NAME="Dual_Record_$(openssl rand -hex 3)"
PACKAGE_NAME="com.example.$PROJECT_NAME"

由于每个包名不能重复,因此使用openssl rand -hex 3生成一串随机字符,并将生成结果直接拼接至字符串末尾。

1
2
3
4
5
6
# 创建基础的项目结构
mkdir -p "./$PROJECT_NAME/app/src/main/java/$PACKAGE_NAME"
mkdir -p "./$PROJECT_NAME/app/src/main/res/layout"
mkdir -p "./$PROJECT_NAME/app/src/main/res/mipmap"
echo "rootProject.name='$PROJECT_NAME'" > "./$PROJECT_NAME/settings.gradle"
echo "include ':app'" > "./$PROJECT_NAME/settings.gradle"

安卓或JAVA项目一般需要遵循一些固定的项目文件夹规范,因此需要提前mkdir -p直接创建层级文件夹。创建完毕后,再往settings.gradle项目配置中写入项目信息。

1
2
3
# 复制图标到项目中
cp /Users/project256/Downloads/5a1acc930a4f210f5ee9e73e45bec3c7_1.png "./$PROJECT_NAME/app/src/main/res/mipmap/ic_launcher.png"
cp /Users/project256/Downloads/5a1acc930a4f210f5ee9e73e45bec3c7_1.png "./$PROJECT_NAME/app/src/main/res/mipmap/ic_launcher_round.png"

使用cp命令复制自定图标到目标文件夹中并使用指定文件名。

1
2
3
4
5
6
7
8
# 创建strings.xml文件并添加app_name字符串资源
mkdir -p "./$PROJECT_NAME/app/src/main/res/values/"
cat > "./$PROJECT_NAME/app/src/main/res/values/strings.xml" <<EOF
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">APP名字</string>
</resources>
EOF

使用cat > “文件” <<EOF,将多行字符串写入到指定文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# This goes in the project-level build.gradle creation step
cat > "./$PROJECT_NAME/build.gradle" <<EOF
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
// Make sure to use a compatible version
classpath 'com.android.tools.build:gradle:7.4.2'
}
}

allprojects {
repositories {
google()
mavenCentral()
}
}

task clean(type: Delete) {
delete rootProject.buildDir
}
EOF

build.gradle中指定了编译项目时需要执行的动作,注意gradle的版本要和本机保持一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Adjusted AndroidManifest.xml creation with android:exported="true"
cat > "./$PROJECT_NAME/app/src/main/AndroidManifest.xml" <<EOF
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="$PACKAGE_NAME">

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.AppCompat.Light.NoActionBar">
<activity android:name=".MainActivity"
android:exported="true"> <!-- Ensure this line is added -->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>
EOF

将安卓核心信息写入AndroidManifest.xml,包含图标、主题等信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    # 创建一个简单的MainActivity
cat > "./$PROJECT_NAME/app/src/main/java/$PACKAGE_NAME/MainActivity.java" <<EOF
package $PACKAGE_NAME;

import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;

public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
EOF

MainActivity.java是应用执行的程序入口,这里显示了主视图即完成工作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
    # 创建一个简单的activity_main.xml布局文件
cat > "./$PROJECT_NAME/app/src/main/res/layout/activity_main.xml" <<EOF
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="欢迎使用XX系统_$PROJECT_NAME!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
EOF

就算最简单的安卓应用也需要设置布局,布局在activity_main.xml中指定。这里只设置一个TextView作为展示。

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
# 创建app模块的build.gradle文件
cat > "./$PROJECT_NAME/app/build.gradle" <<EOF
apply plugin: 'com.android.application'

android {
compileSdkVersion 31
defaultConfig {
applicationId "$PACKAGE_NAME"
minSdkVersion 21
targetSdkVersion 31
versionCode 1
versionName "1.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}

dependencies {
implementation 'androidx.appcompat:appcompat:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}
EOF

子模块的build.gradle中配置了安卓编译的版本及依赖信息,像这种简单的应用也不会依赖太多的东西。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 初始化Gradle Wrapper
cd "$PROJECT_NAME"
gradle wrapper --gradle-version=7.5
cd ..

# 构建项目
cd "$PROJECT_NAME"
./gradlew assembleDebug
cd ..

# 复制APK到脚本执行目录
cp ./"$PROJECT_NAME"/app/build/outputs/apk/debug/app-debug.apk ./"$PROJECT_NAME.apk"

echo "Project $PROJECT_NAME has been created and packaged."

最后进入目录中,使用gradle打包、构建项目,并复制apk到bash执行目录中,方便后续统一收集、上传、部署。

至此一个能够批量生成安卓APK包的Bash脚本就做好了,通过脚本化,让LLM和机器给我打工,节省了大量的时间。

在外行人看来,苦劳和规模很大,就可以多要点价,所以工具自动化能够让副业的开展如虎添翼。