學得懂的 Android Framework 教程——玩轉 AOSP 之系統 App 原始碼新增

語言: CN / TW / HK

1. 如何新建一個系統 App 專案

使用 Android Studio 新建一個空專案 FirstSystemApp,包名設定為 com.yuandaima.firstsystemapp,語言選擇 Java。後面為敘述方便稱該專案為 as 專案。

接著在 Jelly/Rice14 目錄下建立如下的目錄和檔案:

接著將 as 專案中的 res 檔案下的資原始檔拷貝到 Jelly/Rice14/FirstSystemApp/res 中,把 as 專案中的 MainActivity.java 拷貝到 Jelly/Rice14/FirstSystemApp/src/com/yuandaima/firstsystemapp 中。

接著修改已新增的 AndroidManifest.xml 檔案:

```xml

<application
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:supportsRtl="true"
    android:theme="@style/Theme.FirstSystemApp">
    <activity
        android:name=".MainActivity"
        android:exported="true">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />

            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>

        <meta-data
            android:name="android.app.lib_name"
            android:value="" />
    </activity>
</application>

```

接著修改已新增的 Android.bp 檔案:

```json android_app { name: "FirstSystemApp",

srcs: ["src/**/*.java"],

resource_dirs: ["res"],

manifest: "AndroidManifest.xml",

sdk_version: "current",

certificate: "platform",

product_specific: true,

//依賴
static_libs: ["androidx.appcompat_appcompat",
             "com.google.android.material_material",
             "androidx-constraintlayout_constraintlayout"],

} ```

至此我們的系統 App 就建立好了。

接著在我們的 Product 中新增這個App,修改 device/Jelly/Rice14/Rice14.mk

```Makefile

新增以下內容

PRODUCT_PACKAGES += FirstSystemApp ```

接著編譯系統,啟動虛擬機器,開啟 app:

bash source build/envsetup.sh lunch Rice14-eng make -j16 emulator

2. 系統 App 與 普通 App 的差異

2.1 系統 App 可以使用更多的 api

當我們在 Android.bp 中配置了:

platform_apis: true, sdk_version: "",

當 platform_apis 為 true 時,sdk_version 必須為空。這種情況下我們的 app 會使用平臺 API 進行編譯而不是 SDK,這樣我們的 App 就能訪問到非 SDK API 了。關於 SDK API 和非 SDK API 的內容可以參考官方文件

2.2 系統 App 的簽名

AOSP 內建了 APK 簽名檔案,我們可以在 Android.bp 中通過 certificate 配置系統 app 的簽名檔案,certificate 的值主要有一下幾個選項:

  • testkey:普通 APK,預設情況下使用
  • platform:該 APK 完成一些系統的核心功能。經過對系統中存在的資料夾的訪問測試,這種方式編譯出來的 APK 所在程序的 UID 為system
  • shared:該 APK 需要和 home/contacts 程序共享資料
  • media:該 APK 是 media/download 系統中的一環
  • PRESIGNED:表示 這個 apk 已經簽過名了,系統不需要再次簽名;

2.3 系統 App 能使用更多的許可權

當 Android.bp 中的 privileged 被配置為 true 時,我們的系統 App 在新增特許許可權許可名單後,能使用 signature 和 signatureOrSystem 級別的許可權,而普通 App 是不能使用這些許可權的。

在後續的系統許可權相關的分享中會介紹如何給系統 App 新增特許許可權許可名單

3. 系統 App 新增依賴

1. 新增 AOSP 中已有的庫

在 FirstSystemApp 的 Android.bp 中我們添加了很多依賴:

```json static_libs: ["androidx.appcompat_appcompat", "com.google.android.material_material", "androidx-constraintlayout_constraintlayout"],

```

在 AOSP 中, 很多常用的庫均以預編譯模組的方式新增到系統原始碼中。比如常用的 AndroidX 庫定義在 prebuilts/sdk/current/androidx 目錄下。這些庫通過 prebuilts/sdk/current/androidx/Android.bp 引入。比如 recyclerview 庫的引入方式如下:

json android_library { name: "androidx.recyclerview_recyclerview", sdk_version: "31", apex_available: [ "//apex_available:platform", "//apex_available:anyapex", ], min_sdk_version: "14", manifest: "manifests/androidx.recyclerview_recyclerview/AndroidManifest.xml", static_libs: [ "androidx.recyclerview_recyclerview-nodeps", "androidx.annotation_annotation", "androidx.collection_collection", "androidx.core_core", "androidx.customview_customview", ], java_version: "1.7", }

可以看到引入的是一個 android_library,名字叫 androidx.recyclerview_recyclerview。maifest 檔案在 manifests/androidx.recyclerview_recyclerview/ 目錄下,進入這個目錄只有一個 AndroidManifest.xml 檔案,其內容如下:

```xml

<uses-sdk
    android:minSdkVersion="14"
    android:targetSdkVersion="28" />

```

很奇怪,並沒有看到 RecyclerView 庫的原始碼,也沒有看到 aar 庫檔案。我們接著看 Android.bp 中的依賴,其中一項是 androidx.recyclerview_recyclerview-nodeps,我們在 Android.bp 中看一下它的引入方式:

json android_library_import { name: "androidx.recyclerview_recyclerview-nodeps", aars: ["m2repository/androidx/recyclerview/recyclerview/1.1.0-alpha07/recyclerview-1.1.0-alpha07.aar"], sdk_version: "current", min_sdk_version: "14", static_libs: [ "androidx.annotation_annotation", "androidx.collection_collection", "androidx.core_core", "androidx.customview_customview", ], }

這裡看到了,它的 aar 庫在這裡: m2repository/androidx/recyclerview/recyclerview/1.1.0-alpha07/recyclerview-1.1.0-alpha07.aar

繼續查閱我們可以發現,prebuilts/tools/common/m2 目錄下引入了大量的三方庫。

總結一下,當我們的系統 App 需要引入一個庫的時候,通常會在 prebuilds 目錄下查詢:

  • androidx 相關庫引入,先在 prebuilts/sdk/current/androidx 下尋找配置好的 bp 檔案
  • 其他庫引入,先在 prebuilts/tools/common/m2 下尋找尋找配置好的 bp 檔案

都沒有,就得自己引入了

2. 自己給 AOSP 新增庫

java 庫原始碼引入

這部分參考之前分享的玩轉 AOSP 之自定義模組新增新增自定義模組之 Java 庫和可執行程式章節

java 庫以 jar 包形式引入

這部分參考之前分享的玩轉 AOSP 之預編譯模組新增JAR 包章節

Android 庫原始碼引入

device/Jelly/Rice14 目錄下建立如下的檔案和資料夾

其中 MyCustomView.java 是一個用於演示的沒有具體功能的自定義 View:

```java package com.yuandaima.firstsystemandroidlibrary;

import android.content.Context; import android.util.AttributeSet; import android.view.View;

import androidx.annotation.Nullable;

public class MyCustomView extends View { public MyCustomView(Context context) { super(context); }

public MyCustomView(Context context, @Nullable AttributeSet attrs) {
    super(context, attrs);
}

public MyCustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
}

public MyCustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
    super(context, attrs, defStyleAttr, defStyleRes);
}

} ```

AndroidManifest.xml 的內容如下:

```xml

```

Android.bp 的內容如下:

```json android_library {

//......

//依賴
static_libs: ["androidx.appcompat_appcompat",
             "com.google.android.material_material",
             "androidx-constraintlayout_constraintlayout",
             "FirstSystemAndroidLibrary"],

} ```

修改一下 MainActivity,在 App 裡使用我們的自定義 View:

```java package com.yuandaima.firstsystemapp;

import androidx.appcompat.app.AppCompatActivity;

import android.os.Bundle;

import com.yuandaima.firstsystemandroidlibrary.MyCustomView;

public class MainActivity extends AppCompatActivity {

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    MyCustomView myView = new MyCustomView(this);
}

} ```

接著編譯系統,啟動虛擬機器,開啟 app:

bash source build/envsetup.sh lunch Rice14-eng make -j16 emulator

這樣我們的庫就算引入完畢了。

Android 庫以 aar 包形式引入

更多的時候 Android 庫是以 aar 包的形式引入。

假設我們的 SourceApp 需要引入 lottie 這個動畫庫。

首先我們這裡下載好 lottie 庫的 aar 打包檔案。

device/Jelly/Rice14 目錄下建立如下的目錄結構:

bash liblottie/ ├── Android.bp └── lottie-5.2.0.aar

其中 Android.bp 的內容如下:

bash android_library_import { name: "lib-lottie", aars: ["lottie-5.2.0.aar"], sdk_version: "current", }

然後我們修改 FirstSystemApp 中的 Android.bp 引入這個庫:

```json

static_libs: ["androidx.appcompat_appcompat",
             "com.google.android.material_material",
             "androidx-constraintlayout_constraintlayout",
             "FirstSystemAndroidLibrary",
              "lib-lottie"],

```

這樣就可以在 App 中使用 lottie 庫了

JNI 專案

Android 10 下,Android.bp(soong) 方式對 JNI 的支援有點問題,所以我們只有用 Android.mk 來演示了。Android 13 下 Android.bp (soong) 是完美支援 JNI 的。

device/Jelly/Rice14 目錄下新增如下的檔案與資料夾:

jni/Android.mk 內容如下:

```Makefile LOCAL_PATH:= $(call my-dir) include $(CLEAR_VARS)

LOCAL_MODULE_TAGS := optional

This is the target being built.

LOCAL_MODULE:= myjnilib

All of the source files that we will compile.

LOCAL_SRC_FILES:= \ native.cpp

All of the shared libraries we link against.

LOCAL_LDLIBS := -llog

No static libraries.

LOCAL_STATIC_LIBRARIES :=

LOCAL_CFLAGS := -Wall -Werror

LOCAL_NDK_STL_VARIANT := none

LOCAL_SDK_VERSION := current

LOCAL_PRODUCT_MODULE := true

include $(BUILD_SHARED_LIBRARY)

```

jni/native.cpp 的內容如下:

```c++ / * Copyright (C) 2008 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. /

define LOG_TAG "simplejni native.cpp"

include

include

include "jni.h"

define ALOGV(...) android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS)

define ALOGD(...) android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS)

define ALOGI(...) android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS)

define ALOGW(...) android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS)

define ALOGE(...) android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS)

static jint add(JNIEnv /env/, jobject /thiz*/, jint a, jint b) { int result = a + b; ALOGI("%d + %d = %d", a, b, result); return result; }

static const char *classPathName = "com/example/android/simplejni/Native";

static JNINativeMethod methods[] = { {"add", "(II)I", (void*)add }, };

/ * Register several native methods for one class. / static int registerNativeMethods(JNIEnv env, const char className, JNINativeMethod* gMethods, int numMethods) { jclass clazz;

clazz = env->FindClass(className);
if (clazz == NULL) {
    ALOGE("Native registration unable to find class '%s'", className);
    return JNI_FALSE;
}
if (env->RegisterNatives(clazz, gMethods, numMethods) < 0) {
    ALOGE("RegisterNatives failed for '%s'", className);
    return JNI_FALSE;
}

return JNI_TRUE;

}

/ * Register native methods for all classes we know about. * * returns JNI_TRUE on success. / static int registerNatives(JNIEnv* env) { if (!registerNativeMethods(env, classPathName, methods, sizeof(methods) / sizeof(methods[0]))) { return JNI_FALSE; }

return JNI_TRUE; }

// ----------------------------------------------------------------------------

/ * This is called by the VM when the shared library is first loaded. /

typedef union { JNIEnv env; void venv; } UnionJNIEnvToVoid;

jint JNI_OnLoad(JavaVM vm, void /reserved/) { UnionJNIEnvToVoid uenv; uenv.venv = NULL; jint result = -1; JNIEnv* env = NULL;

ALOGI("JNI_OnLoad");

if (vm->GetEnv(&uenv.venv, JNI_VERSION_1_4) != JNI_OK) {
    ALOGE("ERROR: GetEnv failed");
    goto bail;
}
env = uenv.env;

if (registerNatives(env) != JNI_TRUE) {
    ALOGE("ERROR: registerNatives failed");
    goto bail;
}

result = JNI_VERSION_1_4;

bail: return result; } ```

SimpleJNI.java 的內容如下:

```java

package com.example.android.simplejni;

import android.app.Activity; import android.os.Bundle; import android.widget.TextView;

public class SimpleJNI extends Activity { /* Called when the activity is first created. / @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); TextView tv = new TextView(this); int sum = Native.add(2, 3); tv.setText("2 + 3 = " + Integer.toString(sum)); setContentView(tv); } }

class Native { static { // The runtime will add "lib" on the front and ".o" on the end of // the name supplied to loadLibrary. System.loadLibrary("simplejni"); }

static native int add(int a, int b);

}

```

最外面的 Android.mk 的內容如下:

```Makefile TOP_LOCAL_PATH:= $(call my-dir)

Build activity

LOCAL_PATH:= $(TOP_LOCAL_PATH) include $(CLEAR_VARS)

LOCAL_MODULE_TAGS := optional

LOCAL_SRC_FILES := $(call all-subdir-java-files)

LOCAL_PACKAGE_NAME := JNIApp

LOCAL_JNI_SHARED_LIBRARIES := myjnilib

LOCAL_PROGUARD_ENABLED := disabled

LOCAL_SDK_VERSION := current

LOCAL_DEX_PREOPT := false

LOCAL_PRODUCT_MODULE := true

include $(BUILD_PACKAGE)

============================================================

Also build all of the sub-targets under this one: the shared library.

include $(call all-makefiles-under,$(LOCAL_PATH)) ```

AndroidManifest.xml 的內容如下:

xml <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example.android.simplejni"> <application android:label="Simple JNI"> <activity android:name="SimpleJNI"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>

最後在 device/Jelly/Rice14/Rice14.mk 中新增:

Makefile PRODUCT_PACKAGES += helloworld \ JNIApp \

編譯並執行虛擬機器就可以看到 JNIApp 了:

JNIApp 連結自定義庫

我們這裡嘗試修改 JNIApp,讓其引用到我們的 libmymath 庫。

修改 JNIApp/jni/Android.mk:

```Makefile

新增以下內容

LOCAL_SHARED_LIBRARIES := libmymath ```

修改 JNIApp/jni/native.cpp:

```c++

include "my_math.h"

static jint add(JNIEnv /env/, jobject /thiz*/, jint a, jint b) { int result = a + b; result = my_add(result, result); ALOGI("%d + %d = %d", a, b, result); return result; }

```

然後編譯系統,發現報以下錯誤:

bash error: myjnilib (native:ndk:none:none) should not link to libmymath (native:platform)

可以看出是編譯平臺不一致導致的,修改 JNIApp/jni/Android.mk:

```Makefile

下面這行註釋掉即可

LOCAL_SDK_VERSION := current

```

最後重新編譯,執行虛擬機器即可

參考資料