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" 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 > "./$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 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 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 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 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 cd "$PROJECT_NAME " gradle wrapper --gradle-version=7.5 cd .. cd "$PROJECT_NAME " ./gradlew assembleDebug cd .. 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写脚本了,借着这个项目来学习学习~
第一行,指定了bash执行器位置,一般./xxx.sh的时候会读取第一行。
第二行,设定了一个变量,用来指定生成项目数量。
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 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 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 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 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 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 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 cd "$PROJECT_NAME " gradle wrapper --gradle-version=7.5 cd ..cd "$PROJECT_NAME " ./gradlew assembleDebug cd ..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和机器给我打工,节省了大量的时间。
在外行人看来,苦劳和规模很大,就可以多要点价,所以工具自动化能够让副业的开展如虎添翼。