最近、MTKプラットフォームマシンのシャットダウン目覚まし時計に関連するバグを修正しており、MTKシャットダウン目覚まし時計の実装原理を簡単に理解しています。MTKは、フレームワークのカスタマイズと変更に比較的侵襲性の低いスキームを採用していることがわかります。これは、このスキームの単純な分析です。
AlarmMangerはシャットダウン目覚まし時計を実装します
長い間、Androidシステムのアラームを担当するコンポーネントであるAlarmManagerには、目覚まし時計をシャットダウンするネイティブ実装がありませんでした。Googleの観点からは理にかなっていますが、結局のところ、ユーザーがオフにした後、ユーザーのすべての目覚まし時計を自動的にオンにする必要はありません。幸い、さまざまなSOCメーカーが、OEMに配信されるコードに独自のシャットダウン目覚まし時計の実装を追加します。原則は類似しており、この記事では説明しません。
Androidのシステムサービスのほとんどは上位層にXXXManager
公開されており、XXXManagerService
によって実装されています。目覚まし時計をシャットダウンする要件では、主に次のカテゴリの変更が含まれます。
frameworks/base/core/java/android/app/AlarmManager.java
frameworks/base/services/core/java/com/android/server/AlarmManagerService.java
シャットダウン目覚まし時計の場合、変更のアイデアは実際には非常に単純AlarmManager.java
です。にtype
シャットダウン目覚まし時計AlarmManagerService
として実装します。
ほとんどのメーカー:直接変更、シンプルで失礼
現時点では、ほとんどのメーカーが上記の2つのカテゴリに直接大幅な変更を加える傾向があります。ご存知のとおり、Googleは毎年新しいバージョンのAOSPをリリースしています。メーカーによるさまざまな魔法の変更の後、これらの人気のあるクラスでは、マージの変更が行われるときに多くの競合が発生することがよくあります。競合を解決する人は、Javaから始めることができるだけではありません。コードレベル競合に対処するには、開始する前に、AlarmManagerのコード原則全体を読む必要がある可能性が高くなります。結局のところ、タイマーは非常に基本的なコンポーネントです。正確でないかトリガーされない場合は、惨めになる。
さらに、単一のモジュールであれば問題ありませんが、Androidには何百もの同様のコンポーネントがあるため、可能な限り実行するように変更する場合は、比較的煩わしくないソリューションを使用する必要があります。AOSPを維持しながら実装では、独自のロジックを追加すると同時に、次回AOSPコードをマージするときに迅速なアップグレードを実現できます。
MTK:インストルメンテーション、空のメソッド挿入、実行時の特定の実装と同様
AOSPシステムサーバー全体に対するMTKの変更のほとんどすべてが、インストルメンテーションと同様の方法を使用します。
最初に独自のスタブコードをAOSPサービスに挿入し、次にデフォルトの空の実装を残し、次にサービスを継承し、実際の実装を独自の継承クラスに書き込み、最後にこれらの継承クラスを別のクラスに配置mediatek-framework.jar
します。反映されている場合はクラスに実装され、反映できない場合はネイティブに実装され、コンパイルに失敗することはありません。
このように、AOSPの変更には、「パイルポイント+空の実装」のみが含まれます。これは、すべてのパイルポイントが独自に追加され、特定の実装が混乱しないためです。これにより、後のマージAOSPに大きなメリットがもたらされます。最新のAndroidバージョンにすばやく簡単にアップグレードするのに非常に便利です。
以下に、例としてシャットダウン目覚まし時計を取り上げます。
上記のアイデアによると、開発者にとって、AlarmManager
ほとんどは set(@AlarmType int type, long triggerAtMillis, PendingIntent operation)
メソッドを介して設定されるため、MTKはAlarmManager.java
最初AlarmType
に目覚まし時計をオフにするかどうかを区別するために1つ追加しました。
public class AlarmManager {
/**
* M: This alarm type is used to set an alarm that would be triggered if device
* is in powerOff state. It is set to trigger POWER_OFF_ALARM_BUFFER_TIME ms earlier
* than the actual alarm time so that phone is in wakeup state when actual alarm
* triggers
*/
/** @hide */
public static final int PRE_SCHEDULE_POWER_OFF_ALARM = 7;
///@}
}
次にAlarmManagerService.java
、コードを変更し、スタブを挿入しますが、特定の実装は行いません。
public class AlarmManagerService extends SystemService {
void setImpl(int type, long triggerAtTime, long windowLength, long interval,
PendingIntent operation, IAlarmListener directReceiver, String listenerTag,
int flags, WorkSource workSource, AlarmManager.AlarmClockInfo alarmClock,
int callingUid, String callingPackage) {
//AOSP 省略..//
/// M: added for powerOffAlarm feature @{
if(!schedulePoweroffAlarm(type,triggerAtTime,interval,operation,directReceiver,
listenerTag,workSource,alarmClock,callingPackage)){
// FLYME: linshen@SHELL Continue setting a normal alarm if triggerAtTime can not meet
// phone-off alarm requirements. The specification can be found at MtkAlarmManagerService {@
Slog.w(TAG, "triggerAtTime does not meet phone-off alarm requirements, this alarm "
+ "will not boot up your phone automatically.");
// return;
// @}
}
///@}
/// M: update for powerOffAlarm feature issue @{
if(isPowerOffAlarmType(type)) {
type=RTC_WAKEUP;
}
///@}
}
/// M: added for powerOffAlarm feature @{
protected boolean isPowerOffAlarmType(int type){
return false;
}
protected boolean schedulePoweroffAlarm(int type,long triggerAtTime,long interval,
PendingIntent operation,IAlarmListener directReceiver,
String listenerTag,WorkSource workSource,AlarmManager.AlarmClockInfo alarmClock,
String callingPackage){
return true;
}
protected void updatePoweroffAlarmtoNowRtc(){
}
public void cancelPoweroffAlarmImpl(String name) {
}
///@}
}
上記のコードでsPowerOffAlarmType()
は、、、、schedulePoweroffAlarm()
はすべてMTKによって追加されたメソッドですが、実装は空であり、これらの空のメソッドはます。updatePoweroffAlarmtoNowRtc()
cancelPoweroffAlarmImpl()
setImpl()
重点来了,有了桩点 + 空方法,具体实现在哪里呢?答案是在 vendor/mediatek/proprietary/frameworks/base/services/core/java/com/mediatek/server
:
MTK 在这里写了一个 MtkAlarmManagerService
来继承 AlarmManagerService
,并在此具体实现了前面 AlarmManagerService.java
里面的空方法:
public class MtkAlarmManagerService extends AlarmManagerService {
// M: added for powerOffAlarm feature @{
@Override
protected boolean isPowerOffAlarmType(int type){
if(type != PRE_SCHEDULE_POWER_OFF_ALARM)
return false;
else
return true;
}
@Override
protected boolean schedulePoweroffAlarm(int type,
long triggerAtTime,long interval,PendingIntent operation,IAlarmListener directReceiver,
String listenerTag,WorkSource workSource,AlarmManager.AlarmClockInfo alarmClock,
String callingPackage)
{
/// M:add for PowerOffAlarm feature(type 7) for booting phone before actual alarm@{
if (type == PRE_SCHEDULE_POWER_OFF_ALARM) {
if (mNativeData == -1) {
Slog.w(TAG, "alarm driver not open ,return!");
return false;
}
/// M: Extra Logging @{
if (DEBUG_ALARM_CLOCK) {
Slog.d(TAG, "alarm set type 7 , package name " + operation.getTargetPackage());
}
///@}
String packageName = operation.getTargetPackage();
String setPackageName = null;
long nowTime = System.currentTimeMillis();
triggerAtTime = triggerAtTime - POWER_OFF_ALARM_BUFFER_TIME;
if (triggerAtTime < nowTime) {
/// M: Extra Logging @{
if (DEBUG_ALARM_CLOCK) {
Slog.w(TAG, "PowerOff alarm set time is wrong! nowTime = " + nowTime
+ " ; triggerAtTime = " + triggerAtTime);
}
///@}
return false;
}
/// M: Extra Logging @{
if (DEBUG_ALARM_CLOCK) {
Slog.d(TAG, "PowerOff alarm TriggerTime = " + triggerAtTime +" now = " + nowTime);
}
///@}
synchronized (mPowerOffAlarmLock) {
removePoweroffAlarmLocked(operation.getTargetPackage());
final int poweroffAlarmUserId = UserHandle.getCallingUserId();
Alarm alarm = new Alarm(type, triggerAtTime, 0, 0, 0,
interval, operation, directReceiver, listenerTag,
workSource, 0, alarmClock,
poweroffAlarmUserId, callingPackage);
addPoweroffAlarmLocked(alarm);
if (mPoweroffAlarms.size() > 0) {
resetPoweroffAlarm(mPoweroffAlarms.get(0));
}
}
type = RTC_WAKEUP;
}
return true;
}
@Override
protected void updatePoweroffAlarmtoNowRtc(){
final long nowRTC = System.currentTimeMillis();
updatePoweroffAlarm(nowRTC);
}
/**
* For PowerOffalarm feature, this function is used for APP to
* cancelPoweroffAlarm
*/
@Override
public void cancelPoweroffAlarmImpl(String name) {
/// M: Extra Logging @{
if (DEBUG_ALARM_CLOCK) {
Slog.i(TAG, "remove power off alarm pacakge name " + name);
}
///@}
// not need synchronized
synchronized (mPowerOffAlarmLock) {
removePoweroffAlarmLocked(name);
// AlarmPair tempAlarmPair = mPoweroffAlarms.remove(name);
// it will always to cancel the alarm in alarm driver
if (mNativeData != 0 && mNativeData != -1) {
if (name.equals("com.android.deskclock")) {
set(mNativeData, PRE_SCHEDULE_POWER_OFF_ALARM, 0, 0);
}
}
if (mPoweroffAlarms.size() > 0) {
resetPoweroffAlarm(mPoweroffAlarms.get(0));
}
}
///@}
}
看到这里,你可能隐约感觉到上面这个类的路径似乎很眼熟。事实上,MTK 确实使用了一种相对规范的方法来定制框架,他们新建了 vendor/mediatek/proprietary
目录,以此作为自己的源码根目录,如下图所示。
在这个目录下,MTK 参考了 AOSP 根目录的结构创建对应的子目录,每一个目录直接跟 AOSP 对应的目录对应,包含的全是 MTK 自己的实现,这样既方便查找代码位置,也避免了直接将自己的代码实现一股脑儿往 AOSP 里面直接塞。
问题来了,就这样改一下,android 在运行时就自己知道要来这里找具体实现了吗?
当然不是。要查这个问题,我们需要先看一下 vendor/mediatek/proprietary/frameworks/base
,了解一下 base
模块是怎么编译的:
java_library {
name: "mediatek-framework",
installable: true,
libs: [
可以看到,MTK 将他们自己的 framework 编译成一个 mediatek-framework.jar
放到 system/framework
下面,但又在哪里用到呢?
我们知道,Android 在进系统后,系统服务是由 SystemServer
来管理启动的,因此尝试去 frameworks/base/services/java/com/android/server/SystemServer.java
寻找答案:
public final class SystemServer {
///M: for mtk SystemServer @{
private Object mMtkSystemServerInstance = null;
private static Class<?> sMtkSystemServerClass = getMtkSystemServer();
///@}
///M: for mtk SystemServer @{
private static MtkSystemServer sMtkSystemServerIns = MtkSystemServer.getInstance();
///@}
/// M: For mtk system server.
private static Class<?> getMtkSystemServer() {
try {
String className = "com.mediatek.server.MtkSystemServer";
String mtkSServerPackage = "system/framework/mediatek-services.jar";
PathClassLoader mtkSsLoader = new PathClassLoader(mtkSServerPackage,
SystemServer.class.getClassLoader());
return Class.forName(className, false, mtkSsLoader);
} catch (Exception e) {
Slog.e(TAG, "getMtkSystemServer:" + e.toString());
return null;
}
}
private void run() {
//AOSP 省略..//
TimingsTraceAndSlog t = new TimingsTraceAndSlog();
// Start services.
try {
//AOSP 省略..//
/// M: for mtk other service.
sMtkSystemServerIns.startMtkCoreServices();
startOtherServices(t)
} catch (Throwable ex) {
//AOSP 省略..//
} finally {
//AOSP 省略..//
}
}
/**
* Starts a miscellaneous grab bag of stuff that has yet to be refactored and organized.
*/
private void startOtherServices(@NonNull TimingsTraceAndSlog t) {
t.traceBegin("StartAlarmManagerService");
if(!sMtkSystemServerIns.startMtkAlarmManagerService()){
mSystemServiceManager.startService(new AlarmManagerService(context));
}
t.traceEnd();
}
/**
* Starts some essential mtk services that are not tangled up in the bootstrap process.
*/
private void startMtkCoreServices(){
Slog.i(TAG, "startMtkCoreServices start");
try {
if (mMtkSystemServerInstance != null) {
Method method = sMtkSystemServerClass.getMethod("startMtkCoreServices");
method.invoke(mMtkSystemServerInstance);
}
}catch (Exception e) {
Slog.e(TAG, "reflect startMtkCoreServices error" + e.toString());
}
}
/// @}
}
到这里已经很清晰了,MTK 首先修改了 SystemServer.java
,尝试通过反射去获得 mediatek-services.jar
内的 MtkSystemServer
,并且直接获得 sMtkSystemServerIns
, 紧接着在 run()
内部通过 sMtkSystemServerIns
再反射调用对应的 startMtkXXXServices()
方法。
而对于 startMtkAlarmManagerService()
这种相对比较独立,不属于哪一组类型的服务,则直接插桩到 AOSP 的 startOtherServices()
内部,通过拦截的形式修改启动流程,先尝试通过 sMtkSystemServerIns.startMtkAlarmManagerService()
启动自己的 MtkAlarmManagerService
,如果失败了,用 mSystemServiceManager.startService(new AlarmManagerService(context));
启动原生的 AlarmManagerService 来兜底。
接着看一下位于 frameworks/base/services/core/java/com/mediatek/server
的 MtkSystemServer.java
,整个类就100行出头,清晰明了,原理依旧是反射,在 getInstance()
的时候拿到 MtkSystemServerImpl
,所有的 startMtkXXXServices()
方法,因为这是在 frameworks/base
下,所以在这里依旧是空实现,不作具体实现。
public class MtkSystemServer {
private static MtkSystemServer sInstance;
public static PathClassLoader sClassLoader;
public static MtkSystemServer getInstance() {
if (null == sInstance) {
String className = "com.mediatek.server.MtkSystemServerImpl";
String classPackage = "/system/framework/mediatek-services.jar";
Class<?> clazz = null;
try {
sClassLoader = new PathClassLoader(classPackage,
MtkSystemServer.class.getClassLoader());
clazz = Class.forName(className, false, sClassLoader);
Constructor constructor = clazz.getConstructor();
sInstance = (MtkSystemServer) constructor.newInstance();
} catch (Exception e) {
Slog.e("MtkSystemServer", "getInstance: " + e.toString());
sInstance = new MtkSystemServer();
}
}
return sInstance;
}
public boolean startMtkAlarmManagerService() {
return false;
}
public void startMtkCoreServices() {
}
}
那么实现类究竟在哪呢?理所当然我们找到 MtkSystemServerImpl.java
,它位于vendor/mediatek/proprietary/frameworks/base/services/core/java/com/mediatek/server
,还记得前面说的吗?MTK 将自己的实现全部放在这边:
public class MtkSystemServerImpl extends MtkSystemServer {
private static final String MTK_ALARM_MANAGER_SERVICE_CLASS =
"com.mediatek.server.MtkAlarmManagerService";
/**
* Starts MtkAlarmManagerService if existed
*/
@Override
public boolean startMtkAlarmManagerService() {
traceBeginAndSlog("startMtkAlarmManagerService");
try {
startService(MTK_ALARM_MANAGER_SERVICE_CLASS);
} catch (Throwable e) {
Slog.e(TAG, "Exception while starting MtkAlarmManagerService" +e.toString());
return false;
}
traceEnd();
return true;
}
/**
* Starts some essential mtk services that are not tangled up in the bootstrap process.
*/
@Override
public void startMtkCoreServices() {
Slog.i(TAG, "startMtkCoreServices ");
//**MTK, TOO LONG **/
}
//**MTK, TOO LONG **/
}
在这个类里,我们终于看到了喜闻乐见的 MtkAlarmManagerService
,原来是在这里被启动的,还有很多其它 MTK 定制的服务也一并在这里启动。事实上,MTK 在尽量避免把所有的接口、实现、修改全部侵入到 AOSP 的前提下,正是通过这种方式,完成了对 AOSP 绝大多数模块的定制修改。
优缺点分析
没有任何一种方案或架构是完美的,任何时候都一定有它的优缺点。采用 MTK 这种类似插桩的方式对 AOSP 进行定制修改,有如下几点:
- 优点
- 尽可能减少了对 AOSP 代码的大面积入侵
- 对 AOSP 不可避免的修改也只留桩点,逻辑清晰,方便后期合并上游代码做升级
- 桩点的具体实现可单独打包(
mediatek-services.jar
),与 AOSP 完全解耦,方便测试验证问题(即使不把模块编进去也不报错,还可以直接走原生逻辑做到兼容)。 - 理论上可结合 APEX 做模块升级(未验证)
- 缺点
- 结构上减少了入侵,但实际上可能需要修改更多的文件。(比如在本文的例子中,直接改原生的
AlarmManagerService
可更为简单粗暴,但为了实现这种插桩式修改,反而要修改SystemServer
增加启动点,并且新增mediatek-services.jar
模块编译) - 如果 Google 在版本升级后修改了父类方法,继承类有可能需要跟着修改
- 代码可读性降低,因为空实现与具体实现分开在两个库,要跳着看,略微增加了维护成本。
- 结构上减少了入侵,但实际上可能需要修改更多的文件。(比如在本文的例子中,直接改原生的
总结
本文结合 MTK 平台关机闹钟的实现,结合对 MtkAlarmManagerService
的修改,分析了一种对 AOSP 低侵入式修改的方法。值得再次强调的是,这种修改方式的目标并不是为了减少代码修改行数,或是减少文件修改个数,而是旨在修改的时候做到避免大面积入侵,避免将厂商自己的逻辑实现全部填塞到原生代码里,而是通过桩点 + 空实现 + 实际实现的形式来做到低侵入修改,以期在后续维护升级时能感受到便捷,以至快速交付。