Android源码配置第三方应用电池白名单流程分析笔记

这里以MTK6765 Android S举例说明,Android系统在加载客户应用白名单的过程。

首先Android系统可以根据不同手机厂商的需要进行源码的定制,当然定制应用白名单也是可以的,一般情况下在Android源码目录下存在一个Vendor文件夹,该文件夹是Android专门为不同手机厂商定制使用的文件夹,我们可以在里面做一些定制的操作。

一般情况下不同的项目对于白名单的需要是不一样的,所有这里只针对我们公司的某个项目而言其白名单的所在路径是/vendor/xxxxx/product/common_req/xxxx/etc/deviceidle.xml,其内容如图,好了现在我们知道了这个xml里的配置是什么样的了,问题了接下来我们要怎么在系统中去解析这个xml文件哩?

一般情况下我们在编译Android源码的时候是使用脚本命令去编译的,这里我展示我们公司脚本命令的一部分,就是通过PRODUCT_COPY_FILES将/vendor/xxxxx/product/common_req/xxxx/etc/deviceidle.xml的配置文件copy到手机的system/etc文件夹下为接下来framework层的解析做好准备。

 好了之前做了做了这么多的事情终于要到解析的环节了,在Android S解析白名单与之前有一些不同,Android S 使用DeviceIdleController.java中来解析/deviceidle.xml的配置(这个类在 /frameworks/base/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java),可以看到DeviceIdleController是通过一个BroadcastReceiver来接收解析deviceidle,case  Intent.ACTION_PACKAGE_ADDED为我司根据需求解析白名单的逻辑,在收到ACTION_PACKAGE_ADDED的广播之后,通过AtomicFile mConfigFileForJourney = new AtomicFile(new File(getSystemETCDir(), "deviceidle.xml"))将deviceidle.xml读取出来转换成一个AtomicFile,在通过readDefaultConfigFileLocked去解析deviceidle的格式,那么具体是怎么解析的尼?

private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
        @Override public void onReceive(Context context, Intent intent) {
            switch (intent.getAction()) {
                case ConnectivityManager.CONNECTIVITY_ACTION: {
                    updateConnectivityState(intent);
                } break;
                case Intent.ACTION_BATTERY_CHANGED: {
                    boolean present = intent.getBooleanExtra(BatteryManager.EXTRA_PRESENT, true);
                    boolean plugged = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, 0) != 0;
                    synchronized (DeviceIdleController.this) {
                        updateChargingLocked(present && plugged);
                    }
                } break;
                case Intent.ACTION_PACKAGE_REMOVED: {
                    if (!intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
                        Uri data = intent.getData();
                        String ssp;
                        if (data != null && (ssp = data.getSchemeSpecificPart()) != null) {
                            removePowerSaveWhitelistAppInternal(ssp);
                        }
                    }
                } break;
                case  Intent.ACTION_PACKAGE_ADDED: {
                    Slog.d(TAG, "Intent.ACTION_PACKAGE_ADDED received");
                    if (JourneyCustomFeature.DEVICEIDLE_WHITELIST_SUPPORT && !intent.getBooleanExtra(Intent.EXTRA_REPLACING, false)) {
                        //先读取一次配置文件
                        readConfigFileLocked();
                        AtomicFile mConfigFileForCustomer = new AtomicFile(new File(getSystemETCDir(), "deviceidle_customer.xml"));
                        AtomicFile mConfigFileForJourney = new AtomicFile(new File(getSystemETCDir(), "deviceidle.xml"));
                        //根据需要去读取相应的配置文件
                        readDefaultConfigFileLocked(mConfigFileForCustomer);
                        readDefaultConfigFileLocked(mConfigFileForJourney);
                        //更新白名单数据
                        updateWhitelistAppIdsLocked();
                        //通过广播通知PowerSaveWhitelist改变了
                        reportPowerSaveWhitelistChangedLocked();
                        ///通过广播通知临时白名单变化
                        reportTempWhitelistChangedLocked();
                        // report whitelist app to the network
                        if (JourneyDebugMonitor.MONITOR_REPORT) {
                            Uri packageData = intent.getData();
                            String packageSsp;
                            if (packageData != null && (packageSsp = packageData.getSchemeSpecificPart()) != null && getPowerSaveWhitelistAppInternal(packageSsp)) {
                                JourneyDebugMonitor.ReportMonitorEvent(JourneyDebugMonitor.MONITOR_EVENT.IDLE_WHITELIST_APP, packageSsp);
                            }

                        }
                  
                    }
                }
            
            }
        }
    };

 下面是该方法的具体实现,可以看见实现原理很简单,将file的输入流打开,并且将输入流传递到XmlPullParser,通过XmlPullParser来解析xml文件里的item,最后通过readConfigFileLocked函数将里面的内容读到内存里面,那么readConfigFileLocked具体干了些什么尼?我们继续往下看。

void readDefaultConfigFileLocked(AtomicFile file) {
        if (DEBUG) {
            Slog.d(TAG, "Reading config from " + file.getBaseFile());
        }
        //mPowerSaveWhitelistUserApps.clear();
        FileInputStream streamUser;
        try {
            streamUser = file.openRead();
        } catch (FileNotFoundException e) {
            return;
        }
        try {
            //初始化XML解析器
            XmlPullParser parser = Xml.newPullParser();
            //设置输入
            parser.setInput(streamUser, StandardCharsets.UTF_8.name());
            //通过XML解析器来读取xml文件的配置的内容
            readConfigFileLocked(parser);
        } catch (XmlPullParserException e) {
        } finally {
            try {
                streamUser.close();
            } catch (IOException e) {
            }
        }
    }

下面是readConfigFileLocked函数的具体实现逻辑看起来很复杂,其实没有看起来那么复杂就是按照规则去解析xml,但是我们的重点不在这里,我们接着往下看。

private void readConfigFileLocked(XmlPullParser parser) {
        final PackageManager pm = getContext().getPackageManager();

        try {
            int type;
            while ((type = parser.next()) != XmlPullParser.START_TAG
                    && type != XmlPullParser.END_DOCUMENT) {
                ;
            }

            if (type != XmlPullParser.START_TAG) {
                throw new IllegalStateException("no start tag found");
            }

            int outerDepth = parser.getDepth();
            while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
                    && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
                if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
                    continue;
                }

                String tagName = parser.getName();
                switch (tagName) {
                    //需要加入白名单的配置
                    case "wl":
                        String name = parser.getAttributeValue(null, "n");
                        if (name != null) {
                            try {
                               //通过PackageManager获取应用info
                                ApplicationInfo ai = pm.getApplicationInfo(name,
                                        PackageManager.MATCH_ANY_USER);
                                //将获取的应用info put进白名单array
                                mPowerSaveWhitelistUserApps.put(ai.packageName,
                                        UserHandle.getAppId(ai.uid));
                            } catch (PackageManager.NameNotFoundException e) {
                            }
                        }
                        break;
                    case "un-wl":
                        final String packageName = parser.getAttributeValue(null, "n");
                        if (mPowerSaveWhitelistApps.containsKey(packageName)) {
                            mRemovedFromSystemWhitelistApps.put(packageName,
                                    mPowerSaveWhitelistApps.remove(packageName));
                        }
                        break;
                    default:
                        Slog.w(TAG, "Unknown element under <config>: "
                                + parser.getName());
                        XmlUtils.skipCurrentTag(parser);
                        break;
                }
            }

        } catch (IllegalStateException e) {
            Slog.w(TAG, "Failed parsing config " + e);
        } catch (NullPointerException e) {
            Slog.w(TAG, "Failed parsing config " + e);
        } catch (NumberFormatException e) {
            Slog.w(TAG, "Failed parsing config " + e);
        } catch (XmlPullParserException e) {
            Slog.w(TAG, "Failed parsing config " + e);
        } catch (IOException e) {
            Slog.w(TAG, "Failed parsing config " + e);
        } catch (IndexOutOfBoundsException e) {
            Slog.w(TAG, "Failed parsing config " + e);
        }
    }

我们单独看case "wl"的部分,其实这个就是我们在deviceidle.xml里看见的白名单的标签,我们来看看他究竟干了什么,哦原来是通过解析出来的包名去获取ApplicationInfo,然后将包的相关信息put进一个mPowerSaveWhitelistUserApps里面缓存起来了,而mPowerSaveWhitelistUserApps是什么喃,mPowerSaveWhitelistUserApps其实是一个ArrayMap,是Android平台上使用的集合工具。

case "wl":
                        String name = parser.getAttributeValue(null, "n");
                        if (name != null) {
                            try {
                               //通过PackageManager获取应用info
                                ApplicationInfo ai = pm.getApplicationInfo(name,
                                        PackageManager.MATCH_ANY_USER);
                                //将获取的应用info put进白名单array
                                mPowerSaveWhitelistUserApps.put(ai.packageName,
                                        UserHandle.getAppId(ai.uid));
                            } catch (PackageManager.NameNotFoundException e) {
                            }
                        }

 那么现在我们来总结一下配置的白名单加载的过程

  • 在源码/vendor/xxxxx/product/common_req/xxxx/etc/deviceidle.xml中添加白名单配置(项目不同白名单的位置可能也不同)
  • 使用脚本命令将/deviceidle.xml copy到设备system/etc下,等待framework层的解析
  • 在DeviceIdleController通过广播接收解析白名单的命令,并且解析白名单

好了到了这里我们应该对白名单配置到解析的流程有了一个清楚的了解

中场休息:泡个jio,活动一下,稍后再来 

接下来我们来分析一下白名单是如何生效的

既然是电池白名单,所以我们很自然而然的想到Android里管理电池的服务PowerManagerService,所以我们可以去PowerManagerService里查找一个看看可不可以发现什么线索?接下来我们去PowerManagerService查询到了两个int数组mDeviceIdleWhitelist和mDeviceIdleTempWhitelist分别代表全部白名单与临时白名单(感觉这里面保存的应该是非system应用的ID),这说明我们找对了地方,接下来我们来看看他们是在哪里生效的。


// Set of app ids that we will always respect the wake locks for.
int[] mDeviceIdleWhitelist = new int[0];
  
// Set of app ids that are temporarily allowed to acquire wakelocks due to high-pri message
int[] mDeviceIdleTempWhitelist = new int[0];

PowerManagerService我们可以搜索到一个dumpProto函数,里面就有白名单生效的相关代码,但是这个函数实在过于冗长,这里我只粘贴处理一部分来分析。可以看到通过for循环去遍历whitelist然后使用proto去write应用的id,那么ProtoOutputStream这个输出流是什么尼?其实大家可以把Protobuf理解成类似json,xml的可序列化的数据协议就好,这里我们不过多赘述,只需要知道它是Google开发的一种更加高效的协议,而ProtoOutputStream就是Android提供的一个基于Proto格式的输出流,既然ProtoOutputStream是一个输出流,那么这里的whitelist配置输出到哪里了尼?

这里我们注意到了ProtoOutputStream的构造方法的参数是一个FileDescriptor。

private void dumpProto(FileDescriptor fd) {
          final WirelessChargerDetector wcd;
          final ProtoOutputStream proto = new ProtoOutputStream(fd);
 ...........
............
............
for (int id : mDeviceIdleWhitelist) {
     //写入白名单配置
     proto.write(PowerManagerServiceDumpProto.DEVICE_IDLE_WHITELIST, id);
}
for (int id : mDeviceIdleTempWhitelist) {
     //写入临时白名单配置
     proto.write(PowerManagerServiceDumpProto.DEVICE_IDLE_TEMP_WHITELIST, id);              
}
.........
.........
........
}

那么FileDescriptor又是什么尼?这里我们去https://developer.android去查询一下就可以得到相关的答案,对于英语好的小伙伴一定一下就看懂了吧,这里我Google翻译一下大概意思:

文件描述符类的实例用作表示打开文件、打开套接字或另一个字节源或接收器的底层机器特定结构的不透明句柄。 文件描述符的主要实际用途是创建一个 FileInputStream 或 FileOutputStream 来包含它。

应用程序不应创建自己的文件描述符。

FileDescriptor

class FileDescriptor
kotlin.Any
   ↳java.io.FileDescriptor

Instances of the file descriptor class serve as an opaque handle to the underlying machine-specific structure representing an open file, an open socket, or another source or sink of bytes. The main practical use for a file descriptor is to create a FileInputStream or FileOutputStream to contain it.

Applications should not create their own file descriptors.

最后一句话很关键应用程序不应创建自己的文件描述符,为什么应用程序不应创建自己的文件描述符尼?我们都知道Android系统是一个运行在Linux系统上的桌面应用(dogo),Android的底层是基于Linux内核的,而内核会利用文件描述符(file descriptor)来访问文件,那么FileDescriptor fd这个对象又是从哪里传来的尼?我们继续看代码(好累啊。。。),从注释我们可以知道这个dump函数是通过binder来调用获取到FileDescriptor的。

         @Override // Binder call
         protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
              if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
  
              final long ident = Binder.clearCallingIdentity();
  
              boolean isDumpProto = false;
              for (String arg : args) {
                  if (arg.equals("--proto")) {
                      isDumpProto = true;
                  }
              }
              try {
                  if (isDumpProto) {
                      //相关proto配置
                      dumpProto(fd);
                  } else {
                      dumpInternal(pw);
                  }
              } finally {
                  Binder.restoreCallingIdentity(ident);
              }
          }

以上我们对白名单生效应该有了一个大概的认识,我不是很懂Linux,这里只说一下自己的推测,如果有说的不对的地方请指正。首先通过binder从底层获取到FileDescriptor的对象,再通过Protobuf这种通信协议将白名单相关的配置写入到内核层里,然后交由内核去调度?由于本人c++不行就没有追到底层去查看相关的代码了,但是通过这一列源码的分析,我们大概清楚了白名单是如何生效的。

但是!

你以为就完了嘛!其实还没有(第一次写文章好累啊。。。)

还有一个问题我们没有解决,那就是白名单的解析是在DeviceIdleController完成的,生效是在PowerManagerService,那么白名单是如何从DeviceIdleController传递到PowerManagerService的尼?我们继续上代码。在PowerManagerService中定义了setDeviceIdleTempWhitelistInternal函数来设置PowerManagerService中的白名单,那么setDeviceIdleTempWhitelistInternal在哪里调用喃?

      void setDeviceIdleTempWhitelistInternal(int[] appids) {
          synchronized (mLock) {
              mDeviceIdleTempWhitelist = appids;
              if (mDeviceIdleMode) {
                  updateWakeLockDisabledStatesLocked();
              }
          }
      }
  

可以在PowerManagerService中发现这个方法,我们继续寻找这个方法的来源。

         @Override
          public void setDeviceIdleTempWhitelist(int[] appids) {
              setDeviceIdleTempWhitelistInternal(appids);
          }

然后我们在PowerManagerInternal中发现了这个方法的定义,现在我们明白了PowerManagerService是通过PowerManagerInternal中的 setDeviceIdleTempWhitelist方法去获取白名单的。那么哪里调用了PowerManagerInternal的setDeviceIdleTempWhitelist方法喃。

public abstract void setDeviceIdleTempWhitelist(int[] appids);

最后我们又回到了DeviceIdleController中的updateWhitelistAppIdsLocked函数,这里我还是只粘贴我们关心的部分。可以看到就是在这里PowerManagerInternal完成了白名单的set操作。可以看到解析出来的白名单都被整合进了mPowerSaveWhitelistAllAppIdArray里面,然后通过setDeviceIdleWhitelist来更新白名单。

private void updateWhitelistAppIdsLocked() {
        mPowerSaveWhitelistExceptIdleAppIdArray = 
        buildAppIdArray(mPowerSaveWhitelistAppsExceptIdle,
                mPowerSaveWhitelistUserApps, mPowerSaveWhitelistExceptIdleAppIds);
        mPowerSaveWhitelistAllAppIdArray = buildAppIdArray(mPowerSaveWhitelistApps,
                mPowerSaveWhitelistUserApps, mPowerSaveWhitelistAllAppIds);

        mPowerSaveWhitelistUserAppIdArray = buildAppIdArray(null,
                mPowerSaveWhitelistUserApps, mPowerSaveWhitelistUserAppIds);

        if (mLocalActivityManager != null) {
            mLocalActivityManager.setDeviceIdleAllowlist(
                    mPowerSaveWhitelistAllAppIdArray, mPowerSaveWhitelistExceptIdleAppIdArray);
        }
        if (mLocalPowerManager != null) {
            if (DEBUG) {
                Slog.d(TAG, "Setting wakelock whitelist to "
                        + Arrays.toString(mPowerSaveWhitelistAllAppIdArray));
            }
            //PowerManagerInternal添加白名单
            mLocalPowerManager.setDeviceIdleWhitelist(mPowerSaveWhitelistAllAppIdArray);
        }
        passWhiteListsToForceAppStandbyTrackerLocked();
    }

至此整个加载自定义白名单的全过程已经结束了,我们来总结一下整个过程吧。

  • 在源码/vendor/xxxxx/product/common_req/xxxx/etc/deviceidle.xml中添加白名单配置(项目不同白名单的位置可能也不同)
  • 使用脚本命令将/deviceidle.xml copy到设备system/etc下,等待framework层的解析
  • 在DeviceIdleController通过广播接收解析白名单的命令,并且解析白名单
  • 通过buildAppIdArray将白名单整合到里mPowerSaveWhitelistAllAppIdArray,通过PowerManagerInternal的setDeviceIdleWhitelist方法将白名单传递到PowerManagerService中
  • PowerManagerService使用dumproto将白名单的配置通过proto数据协议,写入到对应的Linux FileDescriptor中使之生效。

这里我只是大概梳理一下流程,其实其中还有相当多的细节,由于篇幅有限,这里就不一一说明了,Android源码的代码量是十分巨大的,希望同过这次白名单配置的分析,来加强自己的代码阅读能力与分析能力,也希望本文对大家能有帮助,其中如有不对的地方还望指正。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值