






















目录
基础
| 名称 | 代号 |
|---|---|
AS 开发
local.properties 文件中增加以下两行,其中路径该成你安装 SDK 和 NDK 的路径。
sdk.dir=D\:\\AndroidSDK ndk.dir=D\:\\AndroidSDK\\ndk\\24.0.8215888
Edit Custom VM Options -> 回车,没有则创建之 , 在打开的文件中加入配置: -Dfile.encoding=UTF-8, 然后 双击 shift -> 搜索 restart IED -> 回车 ,
gradle.properties 文件中,或者 ~/.gradle/gradle.properties 中加入下列配置,如果不支持可以使用 # 注释。
systemProp.http.proxyHost=www.somehost.org
systemProp.http.proxyPort=8080
systemProp.http.proxyUser=user
systemProp.http.proxyPassword=password
systemProp.http.nonProxyHosts=localhost
systemProp.http.auth.ntlm.domain=domain
systemProp.https.proxyHost=www.somehost.org
systemProp.https.proxyPort=8080
systemProp.https.proxyUser=user
systemProp.https.proxyPassword=password
systemProp.https.nonProxyHosts=localhost
systemProp.https.auth.ntlm.domain=domain
> Could not resolve all artifacts for configuration ':classpath'.
> Could not resolve com.android.tools.build:gradle:4.1.0.
...
> Could not GET 'https://maven.aliyun.com/nexus/content/groups/public/com/android/tools/build/gradle/4.1.0/gradle-4.1.0.pom'.
...
buildscript {
repositories {
maven{ url 'https://maven.aliyun.com/nexus/content/groups/public/' }
maven{ url 'https://maven.aliyun.com/repository/google' }
maven{ url 'https://maven.aliyun.com/nexus/content/groups/public/'}
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.0'
}
}
allprojects {
repositories {
maven{ url 'https://maven.aliyun.com/nexus/content/groups/public/' }
maven{ url 'https://maven.aliyun.com/repository/google' }
maven{ url 'https://maven.aliyun.com/nexus/content/groups/public/'}
google()
}
}
buildscript {
repositories {
google()
jcenter()
maven { url "https://maven.google.com" }
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.0'
}
}
allprojects {
repositories {
google()
jcenter()
maven { url "https://maven.google.com" }
}
}
gradle,或者工程下的 gradlew 来直接编译,不过你先需要使用 chmod +x 确保你的 gradlew 有可执行权限。
./gradlew task 可以查看当前工程中支持的子任务,如下所示:
> ./gradlew task
> Task :tasks
------------------------------------------------------------
Tasks runnable from root project
------------------------------------------------------------
...
Build tasks
-----------
assemble - Assemble main outputs for all the variants.
assembleAndroidTest - Assembles all the Test applications.
...
./gradlew TASK 的形式来进行相关 task 的执行。
> ./gradlew compileDebugSources
Exception while marshalling /opt/android-sdk/tools/package.xml. Probably the SDK is read-only
...
FAILURE: Build failed with an exception.
* What went wrong:
Could not determine the dependencies of task ':app:compileDebugJavaWithJavac'.
> Failed to install the following Android SDK packages as some licences have not been accepted.
build-tools;30.0.3 Android SDK Build-Tools 30.0.3
platforms;android-29 Android SDK Platform 29
sdkmanager --install "build-tools;30.0.3" 命令和 sdkmanager --install platforms;android-29 来安装相关的哦依赖;
sdkmanager 存在才行,这请参考: https://developer.android.com/studio/command-line
/opt/android/sdk, 但是如果你没有安装 SDK 在该目录或者,你没有权限,那么此时大概率也会报告上述问题。
假如你安装 Android SDK 的目录在 /home/USER/android/sdk, 那么你需要在 local.properties 中声明你使用的 SDK 的实际目录。
如下所示:
> cat local.properties
## This file must *NOT* be checked into Version Control Systems,
# as it contains information specific to your local configuration.
#
# Location of the SDK. This is only used by Gradle.
# For customization when using a Version Control System, please read the
# header note.
#Sun Jul 24 16:58:18 CST 2022
sdk.dir=/home/USER/android/sdk
local.properties 中声明你使用的 Android SDK 或者 NDK 的实际目录。
如下所示:
> cat local.properties
sdk.dir=/home/USER/android/sdk
ndk.dir=/home/USER/android/sdk/ndk-bundle
sdk.dir=~/android/sdk 这样的路径也是不行的。
\\ 取代,防止反转译。比如: sdk.dir=D:\\android\\sdk
命令行工具
frameworks/base/cmds/screencap/
dumpsys 服务名, 服务名可以通过 dumpsys -L 或者 service List 查看。
dumpsys SurfaceFlinger --display-id
Android 系统编译框架
Android 一些基本知识
Android.bp --> Blueprint --> Soong --> Ninja
Makefile or Android.mk --> kati --> Ninja
(Android.mk --> Soong --> Blueprint --> Android.bp)
Android build/envsetup.sh 提供的指令(函数)
一般编译 Android 会首先要执行 source build/envsetup.sh, 该命令会将脚本中的一些脚本函数(操作流程)加载到当前的环境中。 你可以通过直接调用函数名称来执行该过程。该脚本加载的函数包括:
build/soong/soong_ui.bash --dumpvar-mode $1。
AndroidProducts.mk 目录来获得该列表的。
TARGET_PRODUCT, TARGET_BUILD_VARIANT, TARGET_PLATFORM_VERSION
BUILD_TYPE 一般有三个可选的值: user, userdebug, eng。 其分别代表,开放给用限制权限的版本;带有开放 root 权限的可调式版本;带有开发工具/调试工具的版本。
tapas 或者 hmm 可以了解详情。
mmm dir/:target1,target2 来限制构建的对象。
find . -name .repo -prune -o -name .git -prune -o -name out -prune -o -type f \( -name '*.c' -o -name '*.cc' -o -name '*.cpp' -o -name '*.h' -o -name '*.hpp' \)
-exec grep --color -n "$@" {} +
build/envsetup.sh 中的具体实现。
比如这个,命令的具体实现是:
find . -name .repo -prune -o -name .git -prune -o -name out -prune -o -type f -name "*\.gradle" -exec grep --color -n "$@" {} +
pathmod Camera2,就会输出 /home/xxx/xxx/packages/apps/Camera2.
outmod toybox
allmod/gomod/pathmod/outmod/installmod 。
TARGET_DEVICE_DIR 保存当前板级编译配置的路径。
比如我编译的板级为 rk3588_firefly_itx_3588j-userdebug, 那么 TARGET_DEVICE_DIR 的值为 device/rockchip/rk3588/rk3588_firefly_itx_3588j
当你再执行完 source build/envsetup.sh 和 lunch PLT 之后,再运行 make 这时编译系统做了哪些事情?
make 命令会自动加载 Makefile 中的内容,而可能你看到的工程根目录中的 Makefile 只有一行内容,即 include build/make/core/main.mk
而再你打开 include build/make/core/main.mk 文件之后,再文件的前部会看到 main.mk 将 make 的默认 target 改成了 droid, 而 droid target 又进而执行了 droid_targets target。
... .PHONY: droid DEFAULT_GOAL := droid $(DEFAULT_GOAL): droid_targets ... droid_targets : blueprint_tools ...
你可能已经发现了 droid_targets 最终调用到了 blueprint_tools target, 但你找遍整个 core 都找不到 blueprint_tools target 再哪里。 别着急,
关于 Android.mk
PS: 本章以下内容大多取自 极客笔记 相关页面。
CLEAR_VARS 指向一个特殊的 Makefile(可能在这个路径 ./build/make/core/clear_vars.mk ),该命令将会为你清除很多 LOCAL_ 开头的变量,
例如 LOCAL_MODULE、LOCAL_SRC_FILES 和 LOCAL_STATIC_LIBRARIES。
include $(CLEAR_VARS)
./build/make/core/shared_library.mk
build/target/product/base.mk 和 build/target/product/core.mk 里有赋值,这是所有产品都将继承的基础配置,
另外每个设备可在自己的产品配置文件 device_*.mk 里设置该变量,添加更多的模块。
RECOVERY_EXECUTABLES,
UTILITY_EXECUTABLES,
ETC,
STATIC_LIBRARIES,
EXECUTABLES,
FAKE,
JAVA_LIBRARIES,
SHARED_LIBRARIES,
APPS
build/core/base_rules.mk
build/core/base_rules.mk, 其常用取值有:
| 原始表达式 | 例子 |
|---|---|
LOCAL_MODULE_RELATIVE_PATH := ak, 生成的库将会存在于 xxx/ak/ 目录下, xxx 取决于 LOCAL_MODULE_PATH。
LOCAL_CPP_EXTENSION := .cxx .cpp .cc
LOCAL_CPP_FEATURES := rtti features
LOCAL_C_INCLUDES := $(LOCAL_PATH)/xxx/foo
LOCAL_LDLIBS := -lz
LOCAL_LDFLAGS += -fuse-ld=bfd
jni.h not found(出现在 Android 12 某芯片厂商的工程中)。
current 即可。
current, system_current, test_current, core_current,以及具体的版本编号,
关于 Android.bp
APP 下的 gradle
大体框架如下:
apply plugin: 'com.android.application'
android{
compileSdkVersion xx
defaultConfig {
applicationId 'com.xxx.xxx'
minSdkVersion xx
targetSdkVersion xx
versionCode X
versionName xx
externalNativeBuild{
cmake {
}
// or
ndkBuild {
}
}
}
buildTypes {
release {
}
debug {
minifyEnable true/false
proguarFiles
}
}
externalNativeBuild {
cmake {
path '.../CMakeLists.txt'
version '3.xx.x'
}
}
}
dependencies {
...
}
Android init
https://android.googlesource.com/platform/system/core/+/master/init/README.md http://gityuan.com/2016/02/05/android-init/ https://segmentfault.com/a/1190000023184321
init 程序的源码位于 system/core/init 中。而解析 init.rc 文件的函数入口为 parse_config_file。.rc 中的 service 信息会被存放在 server_list 中。
Android Init 文件中主要存在五类对象: Actions、Commands、Services、Options 和 Imports。init 文件中可用 # 进行注释,并且可以使用 ${property.name} 来使用系统属性 (如:import /init.recovery.${ro.hardware}.rc )。
init 程序会在加载之后第一时间加载 /system/etc/init/hw/init.rc 中的配置,随后会加载 /{system,system_ext,vendor,odm,product}/etc/init/ 中的 .rc 文件。
其中,/system/etc/init/ 中存放 Android 核心组件的一些启动配置文件;/vendor/etc/init/ 用于存放厂商的核心 SoC 功能所需的操作或守护程序。 而 /odm/etc/init/ 目录则存放设备制造商的启动文件,如外设或者传感器的守护进程或初始化动作。
Action 和 Service 指示一个 section 的开始,所有的命令或者选项都属于先前定义的最近一个 section , 第一个区块以前的命令和选项将会被忽略。
另外 service 的名字必须被忽略,如果出现第二个同名的 service 名字,那么这个 service 将会被忽略,并且会答应 error 日志。
之前对于 Legacy devices 没有 first stage mount 机制,因此可以在 mount_all 阶段去导入 init 脚本,但是在后续版本中该特性被作废了,并且不允许在 Q 之后启动设备。
system / vendor / odm 分区的所有服务的可执行二进制文件或者脚本都应该在与之相关联的 .rc 文件都应该安装到所在分区的 /etc/init 目录中,并且每一个 .rc 文件应该包含其申明的服务的所有操作。 文件可以申明在 Android.mk 的 LOCAL_INIT_RC 宏中,编译的时候会自动安装到相应的目录。Android.pb 的编译代码如下所示:
prebuilt_etc {
name: "init_recovery.rc",
filename: "init.rc",
src: "etc/init.rc",
sub_dir: "init/hw",
recovery: true,
}
一个 logcat 例子, logcat 的代码位于 system/core/logcat 目录, logcatd.rc 也在其中。 在改目录下有一个 Android.mk 文件,文件中指定了 LOCAL_INIT_RC 宏,在编译的时候将 logcatd.rc 安装到 /system/etc/init 目录中。 init 将在 mount_all 期间加载 logcatd.rc ,而其将会在系统启动过程的适当时机被执行。
每个服务拥有自己 .rc 文件的方式优于以前一个大 init.rc 的方式,一方面在代码的管理方面,冲突会大大减少,另外 .rc 文件跟着工程走,这保证了我们可以非常方便的查找定位问题。
因为 APEX 会应用到多个 Android 主线版本上,此时不同版本的 init.rc 可能不一致,由此在 APEX 中以 init.#rc 的方式命名了各个不同版本的 init.rc 文件,其中 # 为支持的 SDK 版本号(比如 init.rc, init.32rc, init.35rc )。 而在编译的时候,编译器会找出最大的,最接近当前 SDK 版本的 init.rc 安装进去。(如果你的 SDK 为 32,33,34 则会选择 init.32rc, 而如果你的 SDK 版本 >= 35, 那么会选择 init.35rc)
Actions
Action 是一系列指令的名字, Action 中有一个 trigger 字段,该字段标识该 action 在何时被调用。 动作被添加到队列中并根据包含它们的文件被解析的顺序执行(参见 import 部分),然后在单个文件中按顺序执行。
Each action in the queue is dequeued in sequence and each command in that action is executed in sequence. Init handles other activities (device creation/destruction, property setting, process restarting) “between” the execution of the commands in activities.
Action 的通用形式如下所示:
on \trigger [&& \trigger]* \command \command \command
比如下面这个例子:
on boot setprop a 1 setprop b 2 on boot && property:true=true setprop c 1 setprop d 2 on boot setprop e 1 setprop f 2
当 属性 true 的值为 true 时,执行的顺序如下所示:
setprop a 1
setprop b 2
setprop c 1
setprop d 2
setprop e 1
setprop f 2
Services
Services 是一个在开机时需要启动的一个程序,其可以指定是否在程序退出时,自动重启。 Service 的格式如下所示:
service \name \pathname [ \argument ]* \option \option ...
Options
default classname,
/dev/socket/name
dgram, stream, seqpacket 这几个选项
Create a UNIX domain socket named /dev/socket/name and pass its fd to the launched process. type must be “dgram”, “stream” or “seqpacket”. type may end with “+passcred” to enable SO_PASSCRED on the socket. User and group default to 0. ‘seclabel’ is the SELinux security context for the socket. It defaults to the service security context, as specified by seclabel or computed based on the service executable file security context. For native executables see libcutils android_get_control_socket().
参考文档
Service
了解 Service 你可能需要先了解 Binder, 这里有一篇解释 Binder 的文章写得还挺详细的:写给 Android 应用工程师的 Binder 原理剖析
Android 的所有系统服务都在 base/services/java/com/android/server/SystemServer.java 中加载。
SystemServer.java 中包含 java 的函数入口 public static void main(String[] args), zygote 启动的时候会 fork 出 SystemServer 进程,在最后通过反射机制调用到了 SystemServer.main() 函数。
SystemServer 类中的初始化过程如下所示: SystemServer.main -> SystemServer.run -> startBootstrapServices(t) && startCoreServices(t) && startOtherServices(t)。
具体的 SystemServer 启动过程可以参考本章文后参考文档 3。
其中 Android 的 core service 包括, SystemConfigService, BatteryService, UsageStatsService, WebViewUpdateService, CachedDeviceStateService,
BinderCallsStatsService, LooperStatsService, RollbackManagerService, NativeTombstoneManagerService, BugreportManagerService, GpuService。
这些服务都是在 startOtherServices 函数中启动的。
startOtherServices 中启动的 service 有: KeyAttestationApplicationIdProviderService, KeyChainSystemService, SchedulingPolicyService, TelecomLoaderService,
TelephonyRegistry, EntropyMixer( 可能算不上 service ), AccountManagerService$Lifecycle, ContentService$Lifecycle, DropBoxManagerService,
RoleService, VibratorManagerService.Lifecycle, DynamicSystemService, ConsumerIrService, AlarmManagerService, InputManagerService,
DeviceStateManagerService, CameraServiceProxy, WindowManagerService, VrManagerService, BluetoothService,
PinnerService, IorapForwardingService, ProfcollectForwardingService, AppIntegrityManagerService,
MultiClientInputMethodManagerService.Lifecycle, InputMethodManagerService.Lifecycle, AccessibilityManagerService$Lifecycle, StorageManagerService$Lifecycle,
StorageStatsService$Lifecycle, UiModeManagerService, LockSettingsService$Lifecycle, PersistentDataBlockService, TestHarnessModeService,
OemLockService, DeviceIdleController, DevicePolicyManagerService, StatusBarManagerService, MusicRecognitionManagerService,
SpeechRecognitionManagerService, AppPredictionManagerService, ContentSuggestionsManagerService, SearchUiManagerService, SmartspaceManagerService,
FontManagerService.Lifecycle, TextServicesManagerService.Lifecycle,
SystemUpdateManagerService,
UpdateLockService, RkDisplayDeviceManagementService, RkAudioSettingService, NotificationManagerService, DeviceStorageMonitorService,
LocationManagerService, CountryDetectorService, TimeDetectorService$Lifecycle, TimeZoneDetectorService$Lifecycle,
LocationTimeZoneManagerService$Lifecycle, GnssTimeUpdateService$Lifecycle, SearchManagerService$Lifecycle, WallpaperManagerService$Lifecycle,
AudioService.Lifecycle, SoundTriggerMiddlewareService.Lifecycle, BroadcastRadioService, DockObserver, MidiService$Lifecycle,
adb.AdbService$Lifecycle, usb.UsbService$Lifecycle, SerialService, HardwarePropertiesManagerService, TwilightService,
ColorDisplayService, JobSchedulerService, SoundTriggerService, TrustManagerService, BackupManagerService$Lifecycle,
appwidget.AppWidgetService, voiceinteraction.VoiceInteractionManagerService, apphibernation.AppHibernationService, GestureLauncherService,
SensorNotificationService, ContextHubSystemService, DiskStatsService, RuntimeService, RulesManagerService$Lifecycle,
NetworkTimeUpdateService, EmergencyAffordanceService, BlobStoreManagerService, DreamManagerService, GraphicsStatsService,
CoverageService, PrintManagerService, CompanionDeviceManagerService, RestrictionsManagerService, MediaSessionService, HdmiControlService,
TvInputManagerService, TunerResourceManagerService, MediaResourceMonitorService, TvRemoteService, MediaRouterService, FaceService,
IrisService, FingerprintService, BiometricService, AuthService, BackgroundDexOptService, DynamicCodeLoggingService,
PruneInstantAppsJobService, ShortcutService, LauncherAppsService, CrossProfileAppsService, PeopleService, MediaMetricsManagerService,
SliceManagerService$Lifecycle, IoTSystemService, StatsCompanion$Lifecycle, RebootReadinessManagerService$Lifecycle, StatsPullAtomService,
IncidentCompanionService, MmsServiceBroker, AutofillManagerService, TranslationManagerService, ClipboardService, AppBindingService.Lifecycle,
TracingServiceProxy, PermissionPolicyService, R.array.config_deviceSpecificSystemServices 中指定的服务, GameManagerService$Lifecycle,
uwb.UwbService, AppSearchManagerService, MediaCommunicationService, CarServiceHelperService
startOtherServices 中启动的和网络相关的 service 有:
IpConnectivityMetrics, NetworkWatchlistService, NetworkManagementService, IpSecService, NetworkScoreService.Lifecycle,
NetworkStatsService, NetworkPolicyManagerService, wifi.WifiService, wifi.scanner.WifiScanningService, wifi.rtt.RttService,
wifi.aware.WifiAwareService, wifi.p2p.WifiP2pService, lowpan.LowpanService, ethernet.EthernetService, PacProxyService,
ConnectivityServiceInitializer, VpnManagerService, VcnManagementService, NsdService,
startOtherServices 中启动的和手表相关的 service 有:ThermalObserver,
MediaProjectionManagerService, WearPowerService, WearConnectivityService, WearDisplayService,
WearTimeService, WearLeftyService, GlobalActionsService
publishBinderService 和 publishLocalService 区别
参考文档:publishBinderService和publishLocalService区别
摘要: 1. 通过 publishBinderService 方法发布的 Service 上层可以访问对应服务 Manager; 2. 通过 publishLocalService 方法发布的 Service, 上层不可以访问对应服务 Manager, 只能 System 进程访问。
查看 Android 系统中启动的 service 可以使用: service list
public static IBinder getService(String name)
NetworkManagementService
NetworkManagementService 位于 frameworks/base/services/core/java/com/android/server/NetworkManagementService.java 位置。
framework/base/services/java/com/android/server/SystemServer.java 在初始化的时候在 private void startOtherServices(@NonNull TimingsTraceAndSlog t) 函数会初始化网络相关服务,
之后再初始化大多数基础模块之后会调用 NetworkManagementService 进行初始化。
如下所示:
...
networkManagement = NetworkManagementService.create(context);
ServiceManager.addService(Context.NETWORKMANAGEMENT_SERVICE, networkManagement);
...
final NetworkManagementService networkManagementF = networkManagement;
...
if (networkManagementF != null) {
networkManagementF.systemReady();
}
SystemServer 调用 NetworkManagementService.systemReady 之后, systemReady 会调用 private void prepareNativeDaemon() 进行一系列的网络初始化工作。
NetworkManagementService 继承自 INetworkManagementService, INetworkManagementService 定义于 ./frameworks/base/core/java/android/os/INetworkManagementService.aidl 文件中。
NetworkManagementService 提供的接口包括:注册监听网络事件接口; get/set/config 网络接口的接口; 对网络接口进行 IPV6 设置的接口; 操作共享网络的接口; 为 Uid 设置网络接口的权限; 设置防火墙的一些接口; ...
如何打开 NetworkManagerService 的调试信息?方法之一是直接修改 NetworkManagementService.java 中 DBG 成员的值为 true。下面是一个自动编译安装的脚本。
ssh ${PRJ_HOST} "cd ${PRJ_HOME}; source build/envsetup.sh; lunch ${PRJ_PLT}-userdebug; make services -j 32"
scp ${PRJ_HOST}:${PRJ_HOME}/out/target/product/${PRJ_PLT}/system/framework/services.jar /tmp/services.jar
adb root && adb remount && adb push /tmp/services.jar //system/framework/services.jar && adb shell sync && adb reboot
rm -rf /tmp/services.jar
参考文档
Android NetD
简介
https://github.com/Dufre/Android-Settings-Ethernet
Netd 是 Android 系统中专门负责网络管理和控制的后台 daemon 程序,主要三块功能。
Netd 启动的时候会创建这么几个 socket: netd / mdns -- Framework 层中的 NetworkManagementService 和 NsdService 将分别和 netd 及 mdns 监听 socket 建立链接并交互; dnsproxyd -- 每一个调用和域名解析相关的 socket API (如 getaddrinfo 或 gethostbyname 等) 的进程都会借由 dnsproxyd 监听 socket 与 netd 建立链接; fwmarkd -- fwmarkd 和底层 kernel 交互, 防火墙 firewall 会对进来的包做标记。 如下所示:
rk3588_firefly_itx_3588j:/system # cat etc/init/netd.rc
service netd /system/bin/netd
class main
capabilities CHOWN DAC_OVERRIDE DAC_READ_SEARCH FOWNER IPC_LOCK KILL NET_ADMIN NET_BIND_SERVICE NET_RAW SETUID SETGID
socket dnsproxyd stream 0660 root inet
socket mdns stream 0660 root system
socket fwmarkd stream 0660 root inet
onrestart restart zygote
onrestart restart zygote_secondary
# b/121354779: netd itself is not updatable, but on startup it dlopen()s the resolver library
# from the DNS resolver APEX. Mark it as updatable so init won't start it until all APEX
# packages are ready.
updatable
Netd 接收并处理来自 Framework 层中 NetworkManagementService 或 NsdService 的命令。 这些命令最终由 Netd 中对应的 Command 对象去处理。Net 接收并解析来自 Kernel 的 UEvent 消息, 然后再转发给 Framework 层中对应 Service 去处理。
Netd 进程由 init 进程根据 init.rc 的对应配置项而启动。
NetdService
假定你已经熟悉 ServiceManager 相关知识, 假定你已经熟悉基本的网络知识: ethnet, wifi, 七层架构,四层架构,路由,常规网络管理知识, IPV4/IPV6, DNS, DHCP ...
./system/netd 下定义了好几个版本的 INetd.aidl 请仔细甄别使用的是哪一个
final INetd netdInstance = INetd.Stub.asInterface(ServiceManager.getService(Context.NETD_SERVICE));
getInterfaceConfigParcel setInterfaceConfig,
up/down interface setInterfaceUp, setInterfaceDown
hasInterface, updateIpConfiguration, getIpAddress, getNetmask ...
void start() 会调用 mNMService.registerObserver(new InterfaceObserver()); 向 INetworkManagementService 注册 BaseNetworkObserver 观察者。
该 Observer 主要用于监听网络的状态变化(如: interfaceLinkStateChanged, interfaceAdded, interfaceRemoved )。
为解决的问题: framework 各个目录中 net 的关系是如何的
NetD 小知识
main.cpp / gCtls->init(); -> Controllers.cpp / void Controllers::init() -> Controllers.cpp / initIptablesRules();
initIptablesRules 会初始化 INPUT, FORWARD, PREROUTING, POSTROUTING, OUTPUT 的子链
/system/bin/oem-iptables-init.sh 文件中,并同时赋予该文件的可读可执行权限
iptables -I oem_fwd -p all -i eth0 -o eth1 -j ACCEPT; iptables -I oem_fwd -p all -i eth1 -o eth0 -j ACCEPT;
exit 0
oem_fwd / oem_out, 如果还要增加则要改 netd 中 oem_iptables_hook.cpp 部分代码。
netd 如何提供服务的
netd 的服务存在一个类 class NetdNativeService, 该类继承自 public BinderService<NetdNativeService>, public BnNetd, 也意味着它是一个 binder service。
netd 启动的时候执行以下代码启动该服务:
if ((ret = NetdNativeService::start()) != android::OK) { ALOGE("Unable to start NetdNativeService: %d", ret);
exit(1);
}
netd 提供的服务
获取 interface List
调用路径如下所示: @utf8InCpp String[] interfaceGetList(); -> NetdNativeService::interfaceGetList -> InterfaceController::getIfaceNames 。
InterfaceController::getIfaceNames 会打开 /sys/class/net 目录,并查看该目录下的接口有哪些。并将这些接口存储到 std::vector ifaceNames 中,然后返回给调用者。
获取某个 interface 的配置
调用路径如下所示: InterfaceConfigurationParcel interfaceGetCfg(in @utf8InCpp String ifName); -> NetdNativeService::interfaceGetCfg -> InterfaceController::getCfg。
InterfaceController::getCfg 函数会获取到接口的 MAC 地址, IP 地址,掩码, UP/DOWN 等信息。为了获取这些信息,首先要打开一个 socket ( socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0) ), 然后调用 ioctl(fd, SIOCGIFHWADDR, &ifr) 获取接口的硬件地址,调用 ioctl(fd, SIOCGIFADDR, &ifr) 获取接口的 IP 地址以及类型,调用 ioctl(fd, SIOCGIFNETMASK, &ifr) 获取接口掩码, 调用 ioctl(fd, SIOCGIFFLAGS, &ifr) 获取目前接口状态,并通过检查 IFF_UP 查看接口是否被使能。同时还可以检查如下状态 IFF_BROADCAST, IFF_LOOPBACK, IFF_POINTOPOINT, IFF_RUNNING, IFF_MULTICAST。
设置 interface
set 的接口和 get 的接口类似,调用流程如下所示: void interfaceSetCfg(in InterfaceConfigurationParcel cfg) -> NetdNativeService::interfaceSetCfg -> InterfaceController::setCfg 。
set 的时候 ioctl 带的 request id 分别为, SIOCSIFADDR ( 设置 IP 地址 ). SIOCSIFFLAGS ( 将接口 UP or DOWN )。
ndc
ndc 工具是属于 netd 的一部分,在调试 netd 的时候非常有帮助。 netd 工程地址在 system/netd。
其中与 ndc 相关的配置在 system/netd/server/Android.pb 文件中,如下所示:
cc_binary {
name: "ndc", defaults: ["netd_defaults"],
include_dirs: [
"system/netd/include",
],
header_libs: [
"libnetd_client_headers",
],
shared_libs: [
"libbase",
"libnetdutils",
"libnetutils",
"libcutils",
"liblog",
"libutils",
"libbinder",
"dnsresolver_aidl_interface-V7-cpp",
"netd_aidl_interface-V6-cpp",
],
srcs: [
"ndc.cpp",
"UidRanges.cpp",
"NdcDispatcher.cpp",
],
sanitize: {
cfi: true,
memtag_heap: true,
},
}
server/ndc.cpp 中包含了 ndc 的入口,如下所示。
int main(int argc, char** argv){
android::net::NdcDispatcher nd;
exit(nd.dispatchCommand(argc - 1, argv + 1));
}
ndc 支持下表所列的一些指令。 这些指令从 server/NdcDispatcher.cpp 注册到每一个指令的专属类中,这些类的定义在 server/NdcDispatcher.h 中。 ndc 收到命令行输入的数据之后会直接调用 NdcDispatcher::dispatchCommand 方法,该方法会在注册到 NdcDispatcher::mCommands 的命令类中找到对应的命令类, 并调用该命令类的 NdcNetdCommand::runCommand 将指令分发出去。
NdcNetdCommand 为各个命令类的基类,每个派生类都会实现一次 NdcDispatcher::_xxx_::runCommand。 而 runCommand 会负责检查参数的合法性,并调用 sp<INetd> mNetd 通过 binder 将要操作的指令发给 netd 去处理。
| 指令 | 说明 |
|---|---|
下面是一些例子:
ndc interface listndc interface getcfg eth0ndc interface setcfg eth0 downndc interface setcfg eth0 up or ndc interface setcfg eth0 0.0.0.0 up关于 interface 的操作
Camera Provider
camera.provider 位于 hardware/interface/camera/provider 目录。 原始仓库地址:https://android.googlesource.com/platform/hardware/interfaces。 关于 camera 的原始信息可以参考 google 官方仓库中的文档。( source.android.com )
camera.provider 主要用于 camera service 发现,请求,和打开单个 camera 设备。 他还允许直接控制闪光灯,或者打开关闭手电筒模式等。 下面文中的资料也是我直接参考 google 官方的解释以及源码中的注释,如果大家想获得一手的资料,建议还是去查看官方文档。
2.4 是地一个 HIDL 版本, 该版本提供了 HAL 层的操作和回调接口。 之后的版本基本上都是继承字该接口。如下所示: ( HIDL 为 Android 为了描述 service 和 HAL 层调用的接口而设计的一套接口语言 )
// ./2.4/ICameraProvider.hal
interface ICameraProvider {
...
}
// ./2.5/ICameraProvider.hal
interface ICameraProvider extends @2.4::ICameraProvider {
...
}
// ../2.6/ICameraProvider.hal
interface ICameraProvider extends @2.5::ICameraProvider {
...
}
// ./2.7/ICameraProvider.hal
interface ICameraProvider extends @2.6::ICameraProvider {
...
}
下面是个版本 ICameraProvider.hal 提供的接口一览,其中 2.4 版本除了提供了注册 ICameraProviderCallback 毁掉函数的接口之外, 还提供了获取 VendorTag, CameraTags, CameraIdList 的接口,检查是否支持手电筒模式的接口,和获取 CameraDevice 接口的接口。 而后续的版本则补充了发送 notify 的接口,检测是否支持并发流模式的相关接口。在接下来的章节中,将针对各个部分逐一介绍。
> find -name "ICameraProvider.hal" | sort | while read F; do echo "---> $F"; grep "(.*)" $F | grep -v "*" ; done ---> ./2.4/ICameraProvider.halsetCallback(ICameraProviderCallback callback) generates (Status status);getVendorTags() generates (Status status, vec<VendorTagSection> sections);getCameraIdList() generates (Status status, vec<string> cameraDeviceNames);isSetTorchModeSupported() generates (Status status, bool support);getCameraDeviceInterface_V1_x(string cameraDeviceName) generates (Status status, android.hardware.camera.device@1.0::ICameraDevice device);getCameraDeviceInterface_V3_x(string cameraDeviceName) generates (Status status, android.hardware.camera.device@3.2::ICameraDevice device); ---> ./2.5/ICameraProvider.halnotifyDeviceStateChange(bitfield<DeviceState> newState); ---> ./2.6/ICameraProvider.halgetConcurrentStreamingCameraIds() generates (Status status, vec<vec<string>> cameraIds);isConcurrentStreamCombinationSupported(vec<CameraIdAndStreamCombination> configs) generates (Status status, bool queryStatus); ---> ./2.7/ICameraProvider.halisConcurrentStreamCombinationSupported_2_7(vec<CameraIdAndStreamCombination> configs) generates (Status status, bool queryStatus);
ICameraProvider.hal
位于 hardware/interfaces/camera/provider/x.x/ICameraProvider.hal 路径下,主要提供了 setCallback, getVendorTags, getCameraIdList, isSetTorchModeSupported, getCameraDeviceInterface_VN_x 这么几个方法。
Camera provider HAL 可用于枚举独立可用的 camera 设备,并且 provider 还提供更新这些 camera 设备状态的方式。 这些状态包括 connection / disconnection 和手电筒模式的启用和禁用。
provider 有责任提供一个 camera device service 名的 list, 而这些名字对应的 service 必须能被 hardware service manager 打开。
多个 camera provider HAL 也许被集成在一个系统中。为了系统能发现这些 service 的名字和 process 的名字, provider 必须以 android.hardware.camera.provider@<major>.<minor>/<type>/<instance>< 的形式命名。 这里的 <major>.<minor> 为 HAL HIDL 的版本,比如 2.4 / 2.5 ... 而 <type> 则为 provider 的类型,目前已定义的类型有 internal, legacy, external, remote ... 而 camera framework 必须直到各个版本间的差异性。 最后 <instance>, 是一个从 0 开始的非负整数,用于消除相同类型的多个 HAL 之间的歧义。
其中"legacy" 类型的 provider 提供的 device 只能以传统 HAL 模式访问, 而且一定不能被 standalone binderized HAL 所使用。
设备实例名字必须能被 getCameraIdList() 或者 ICameraProviderCallback::cameraDeviceStatusChange() 枚举。 的获取的格式必须服从 "device@<major>.<minor>/<type>/<id><" 其中 <major>/<minor> 为对应的 HIDL 的版本号。 id 是 "internal" 设备类型的一个小的递增整数,0 是主后置摄像头,1 是主前置摄像头(如果存在)。 或者,对于外部设备,一个唯一的序列号(如果可能)可用于在设备断开连接和重新连接时可靠地识别设备。
需要注意的是,多个 provider 一定不能枚举出同一个 device ID。
setCallback
原型:setCallback(ICameraProviderCallback callback) generates (Status status);
该接口用于设置一个回调函数,该回调函数用于同步底层 camera 子系统的事件, camera service 必须在其启动期间,在调用其他 provider 方法前,调用以下该函数。 (当然在 camera service 重启之后也应该重新注册)。
需要注意的是返回值 status 返回的可能值有 OK, INTERNAL_ERROR, ILLEGAL_ARGUMENT。
getVendorTags
原型:getVendorTags() generates (Status status, vec<VendorTagSection> sections);
camera service 可通过该接口获取 所有的 vendor tag。 这些 tag 将会通过 section 字段返回。
需要注意的是返回值 status 返回的可能值有 OK, INTERNAL_ERROR。
getCameraIdList
原型:getCameraIdList() generates (Status status, vec<string> cameraDeviceNames);
该接口给应用层返回一个列表,该列表为 provider 知道的 camera 设备名字。 这些设备都必须能被 hardware service 所管理。
外接摄像头设备必须通过设备状态变化回调上报,并不出现在此列表中。 此处必须仅列出前后摄像头设备。
需要注意的是返回值 status 返回的可能值有 OK, INTERNAL_ERROR。 需要注意的是,如果返回值为 INTERNAL_ERROR 那么很有可能是 camera 子系统初始化失败导致。
isSetTorchModeSupported
原型:isSetTorchModeSupported() generates (Status status, bool support);
该接口不用太多解释,该接口用于检查 camera device 是否支持手电筒模式。 需要注意的是,该接口并不是非常准确,因为并不是每一个 camera 设备都知道自己是否拥有 flash 单元。
getCameraDeviceInterface_VN_x
原型:getCameraDeviceInterface_V1_x(string cameraDeviceName) generates(Status status, android.hardware.camera.device@1.0::ICameraDevice device); 和 getCameraDeviceInterface_V3_x(string cameraDeviceName) generates(Status status, android.hardware.camera.device@3.2::ICameraDevice device);
这不会直接给摄像头设备上电,而只是获取查询设备静态信息的接口,或者额外打开设备进行活动使用。 每一个主版本,返回的相机设备 HAL 都是互相独立的,因为它们彼此不兼容。
提供程序的有效设备名称可以通过 getCameraIdList() 或通过来自 ICameraProviderCallback::cameraDeviceStatusChange() 的可用性回调获得。
返回接口的版本必须是主要版本下的最高(即主版本号相同,次版本最高)。而 HAL 客户端负责处理好次版本存在但实际不支持的方法。
notifyDeviceStateChange
原型:notifyDeviceStateChange(bitfield<DeviceState> newState);。该方法于 2.5 版本加入。
该接口的作用主要用于通知 HAL provider 某些 HAL 层面需要通知的状态变化。比如,物理隐私盖已经被关闭或者打开,又或者与 camera 相关的附加的按键已经按下或者弹起。 这里的状态是潜在状态的 bit 位的状态,一些物理配置可能对应多种不同状态位的组合。HAL 层必须过滤任何没有用到的状态位,一确定正确的配置相机。
例如,在某些设备上, FOLDED 状态可能意味着后向摄像头被折叠覆盖,因此 FOLDED 本身意味着 BACK_COVERED 。 但是其他设备可能支持折叠,但折叠时不会覆盖任何相机,因此对于那些 FOLDED 不会暗示任何其他标志。 由于这些关系是非常特定于设备的,因此很难指定一个全面的策略。 但作为一项建议,如果一个标志必然意味着也设置了其他标志,那么应该设置这些标志。 因此,从清楚准确的角度,即使 FOLDED 足以在某些设备上推断出 BACK_COVERED ,也应该设置 BACK_COVERED 标志。
该方法可能在任何阶段被调用,但我们必须保证其不会导致 camera 或者 session 被关闭,但可能动态的改变 physical camera 和 Logocal multi-camera 的某些属性。
该方法必须在 HAL client 调用 ICameraDevice::open 打开属于这个 provider 的 camera 设备前先调用一次,用来初始化设备的状态。
其中方法中的 newState 为 camera device 的新状态。
getConcurrentStreamingCameraIds
原型: getConcurrentStreamingCameraIds() generates (Status status, vec<vec<string>> cameraIds);, 该方法于 2.6 版本加入。
该接口用于获取一个开流组合的配置列表, 在同时打开多个流的情况下,组合中建议的每一个 camera device 至少要支持下表中的这些组合。
| Target 1 | Target 2 |
|---|---|
这里的 s720p - min (max 最大支持的分辨率为 1280 X 720 ), s1440p - min (最大支持的分辨率为, 1920 X 1440)
如果设备支持 MONOCHROME 能力 ( 通过获取 capabilities 获取到的能力包含 ANDROID_REQUEST_AVAILABLE_CAPABILITIES_MONOCHROME ), 并因此支持 Y8 格式输出,还必须支持上述的流组合,那么其中的 YUV 应该替换为 Y8。
如果 device 的 capabilities 并没有包含 ANDROID_REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE, 那么其必须至少支持单路的 Y16 Stream, sVGA 分辨率的 Dataspace::DEPTH。
camera framework 必须在收到 cameraDeviceStatusChange 得知任何 device 被加入或者移除的时候调用该接口来获取设备的最新状态。 这是为了让相机框架可以即使的获取相机 ID 的出流新组合。
对于每一个从 getConcurrentStreamingCameraIds 获取的 camera 设备 ID 的组合: 如果每个设备只能同时支持强制组合,如果需要确保相机应用程序之间的仲裁按预期工作,则并发集的资源成本总和一定大于 100。 仅当资源足以以全部功能 运行一组摄像机(通过摄像机元数据公开的配置设置中可用的最大资源消 耗帧速率和流大小设置)时,组合的资源成本总和才应该 <= 100。
为了保证 camera 的并发操作,在对目标 camera 进行 stream 配置前, camera framwork 需要先对所有涉及的 camera 调用 ICameraDevice.open(), 这使相机 HAL 进程有机会在流配置之前更好地分配硬件资源。
由于物理相机设备内部切换的潜在硬件限制,设备的完整 ZOOM_RATIO_RANGE ( 如果支持 ) 在并发操作期间可能不适用。 如果支持 ZOOM_RATIO, 则相机 HAL 必须确保在并发操作期间该设备支持 [ 1.0, ANDROID_SCALER_AVAILABLE_MAX_DIGITAL_ZOOM ] 的 ZOOM_RATIO_RANGE。
isConcurrentStreamCombinationSupported
原型: isConcurrentStreamCombinationSupported(vec<CameraIdAndStreamCombination> configs) generates (Status status, bool queryStatus);, 该方法于 2.6 版本加入。
当需要与其他设备一起开流的时候,可用该方法检查设备是否支持指定的组合方式。
每个设备的 streamList 必须包含至少一个可输出的流,但是不能包含多个 input stream。 与常规流配置相比, framwork 不创建或初始化任何实际流。 这意味着 Hal 不得使用或关注 “id” 的值。
下面是一些前提条件:
------------------------------------------------------------------------ * Preconditions: * The framework can call this method at any time before, during and after active session configuration per device. This means that calls must not impact the performance of pending camera requests in any way. In particular there must not be any glitches or delays during normal camera streaming. * The framework must not call this method with any combination of camera ids that is not a subset of the camera ids advertised by getConcurrentStreamingCameraIds of the same provider. * Performance requirements: This call is expected to be significantly faster than stream configuration. In general HW and SW camera settings must not be changed and there must not be a user-visible impact on camera performance. * @param configs a vector of camera ids and their corresponding stream configurations that need to be queried for support. * @return status Status code for the operation, one of: OK: On successful stream combination query. METHOD_NOT_SUPPORTED: The camera provider does not support stream combination query. INTERNAL_ERROR: The stream combination query cannot complete due to internal error. @return true in case the stream combination is supported, false otherwise.
isConcurrentStreamCombinationSupported_2_7
isConcurrentStreamCombinationSupported_2_7(vec configs) generates (Status status, bool queryStatus);
该函数与 @2.6::isConcurrentStreamCombinationSupported 版本中的同名函数相同,只是该函数采用的是 @3.7::StreamConfiguration vector。
configs 参数为一个与查询 camera stream 相关的 camera id 集合。
当返回的 status 值为 OK 的时候请求成功。 而当返回 status 值为 METHOD_NOT_SUPPORTED 的时候,查询的流中包含不支持 stream combination 查询请求。 而当返回 status 值为 INTERNAL_ERROR 的时候则是内部出现错误。
返回值 queryStatus 的值为 true 则代表操作成功, false 代表操作失败。
ICameraProviderCallback.hal
这里 provider 的 CB 函数用于通知 camera service camera 子系统的状态变更。
cameraDeviceStatusChange
原型: cameraDeviceStatusChange(string cameraDeviceName, CameraDeviceStatus newStatus);
cameraDeviceStatusChange: Callback to the camera service to indicate that the state of a specific camera device has changed. On camera service startup, when ICameraProvider::setCallback is invoked, the camera service must assume that all internal camera devices are in the CAMERA_DEVICE_STATUS_PRESENT state. The provider must call this method to inform the camera service of any initially NOT_PRESENT devices, and of any external camera devices that are already present, as soon as the callbacks are available through setCallback. @param cameraDeviceName The name of the camera device that has a new status. @param newStatus The new status that device is in.
torchModeStatusChange
原型: torchModeStatusChange(string cameraDeviceName, TorchModeStatus newStatus);
torchModeStatusChange: Callback to the camera service to indicate that the state of the torch mode of the flash unit associated with a specific camera device has changed. At provider registration time, the camera service must assume the torch modes are in the TORCH_MODE_STATUS_AVAILABLE_OFF state if android.flash.info.available is reported as true via the ICameraDevice::getCameraCharacteristics call. @param cameraDeviceName The name of the camera device that has a new status. @param newStatus The new status that device is in.
vendor tag
vendor tag
SELinux
简介
关于 SELinux 再 Android 中的简要介绍可以查看Android 官方关于 SELinux 的介绍。 AOSP 的 SELinux 仓库位于https://android.googlesource.com/platform/external/selinux/。
如果想要查询 SELinux 的目的,架构,以及其他关于 SELinux 的一切,你可以查看 SELinux Notebook Github 工程, 该工程介绍了:
如果想跳过简介直接查看 SELinux 相关的介绍资料可以访问该文档 https://github.com/SELinuxProject/selinux-notebook/blob/main/src/toc.md。
一些使用 SElinux 的场景
可以参考 Google 官方提供的参考用例 查看。(非完备清单)
一些经验
source build/envsetup.sh 和 lunch xxx-xxx 之后 执行 make selinux_policy 即可单独编译 SELinux 的 Policy
ps -Z
ls -Z
dos2unix 进行文件格式转换。
# te 中存在的规则
allow self_net self_net:udp_socket { create ioctl };
# 报告的警告
avc: denied { ioctl } for path="socket:[47164]" dev="sockfs" ino=47164 ioctlcmd=0x8914 scontext=u:object_r:self_net:s0 tcontext=u:object_r:self_net:s0 tclass=udp_socket permissive=1
avc: denied { ioctl } for path="socket:[50134]" dev="sockfs" ino=50134 ioctlcmd=0x8916 scontext=u:object_r:self_net:s0 tcontext=u:object_r:self_net:s0 tclass=udp_socket permissive=1
# te 中存在的规则
allowxperm ak_net ak_net:udp_socket ioctl { 0x8914 0x8916 };
system/sepolicy/public/te_macros 路径下,其他和平台相关的宏可以在 device 目录使用 find -name "te_macros" 进行搜索。
#!/bin/bash
set -e
PRJ_HOST=l2
PRJ_HOME=./firefly
f_cp_selinux_server_2_device(){
TEMP_F_NAME=/tmp/$$$$
ssh ${PRJ_HOST} "cd ${PRJ_HOME}/out/target/product/rk3588_firefly_itx_3588j/ && ls $1" | while read FILE
do
scp ${PRJ_HOST}:${PRJ_HOME}/out/target/product/rk3588_firefly_itx_3588j/$1/${FILE} ${TEMP_F_NAME}
adb push ${TEMP_F_NAME} //$1/${FILE}
rm -rf ${TEMP_F_NAME}
done
}
adb root && adb remount
f_cp_selinux_server_2_device system/etc/selinux/mapping/
f_cp_selinux_server_2_device system/etc/selinux
f_cp_selinux_server_2_device vendor/etc/selinux
f_cp_selinux_server_2_device odm/etc/selinux
adb reboot
参考资料
什么是规则? 规则就是针对 domain 对象的,上面我们说过,每一个进程都属于一个 domain (域),
规则就是设置哪个 domain (域)的对象(就是进程)能够对哪些 type (类型)的目标对象(文件或者属性或者设备)具有哪有操作(删除啊,访问啊等等),这就是规则!
xxx_defconfig 中; kernel 中关闭的地方在板级的 dts 配置中;
其他
adb root
adb disable-verity
adb reboot
adb root
adb remount
-#define CFG_BRCTL 0
-#define USE_BRCTL(...)
+#define CFG_BRCTL 1 // 使能 brctl
+#define USE_BRCTL(...) __VA_ARGS__ // 将 brctl 添加到 toybox 中
brctl-utils。
Android.bp 文件。Android.bp 如何编写可以参考本章下面 Android.bp 小节。
brctl-utils 编进你的平台中,你需要在你平台相应的 AndroidBoard.mk 中的 PRODUCT_PACKAGES 变量加上该 package 名称。
文件可能在一个类似于这样 device/rockchip/rk3588/xxx 的路径中。
all_srcs = [
"libbridge/libbridge_devif.c",
"libbridge/libbridge_misc.c",
"libbridge/libbridge_init.c",
"libbridge/libbridge_if.c",
"brctl/brctl.c",
"brctl/brctl_cmd.c",
"brctl/brctl_disp.c",
]
cc_binary {
name: "brctl",
// defaults: ["brctl-defaults"],
host_supported: true,
// recovery_available: true,
vendor_ramdisk_available: true,
srcs: all_srcs,
cflags: [
"-Os",
"-Wall",
"-Werror",
"-Wno-unused-parameter",
"-Wno-unused-result",
],
// cppflags: [],
local_include_dirs: [
"libbridge"
],
// header_libs: [ ],
// static_libs: [ ],
// shared_libs: [ ],
// export_include_dirs: [ ],
target: {
android: {
// local_include_dirs: [],
// shared_libs: [],
// symlinks: [],
},
},
}
| 修饰符 | 类内部 | 同个包(package) | 子类 | 其他范围 |
|---|---|---|---|---|
init -> system/core/init -> init.cpp
init.rc -> system/core/rootdir
Android 的基础服务是从 init.zygote*.rc 配置中启动的
Lancher.java -> packages/apps/Lancher*
冷启动用 Instramentation, 热启动用 bindler
AMS -> Instramentation -> Activity/fragment/application

参考文档
原创文章,版权所有,转载请获得作者本人允许并注明出处
我是留白;我是留白;我是留白;(重要的事情说三遍)
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。