电量优化 - Hook 系统服务

上一篇文章《电量优化 - 电量的统计原理与监控》已经讲到了 Android App 电量的计算方式,也分析了系统源码 Android 是怎么统计电量的。那么现在我们可以开始给自己的 App 开发电量异常检测功能了,实现的方案就是用系统源码类似的计算方案,在 App 内部进行电量统计,主要也就两个部分:线程监控与系统服务调用监控。如果大家觉得麻烦的话可以尝试一下我们的开源方案 matrix-battery-canary ,这套方案在我们的项目项目中全量运行了一年多,期间发现了很多电量问题。如果大家感兴趣,这期文章我先带大家来实现系统服务调用监控,后面再带大家实现线程监控。

如何 Hook 系统服务的调用?主流上一般有三种方案:字节码插桩,动态代理,Native Hook。这三种方案我们都有讲过也有用过,这里我们用动态代理来实现,大家可以自己先去试着实现下。套路印象中至少应该讲了十次,第一步肯定首先是要看源码流程了,第二步找单例和接口切入点,第三步就是设计实现类。源码我就简单贴了,因为在 《Framework 源码分析》中都讲到过了:

// 调用一般都是通过 context 获取系统服务
WifiManager mWifi = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
mWifi.startScan();

对应找到 /frameworks/base/core/java/android/app/ContextImpl.java 中的 getSystemService 方法

    @Override
    public Object getSystemService(String name) {
        return SystemServiceRegistry.getSystemService(this, name);
    }

再找到 /frameworks/base/core/java/android/app/SystemServiceRegistry.java 中的 getSystemService 方法

    private static final HashMap<String, ServiceFetcher<?>> SYSTEM_SERVICE_FETCHERS = new HashMap<String, ServiceFetcher<?>>();
    
    static {
        registerService(Context.WIFI_SERVICE, WifiManager.class,
                new CachedServiceFetcher<WifiManager>() {
            @Override
            public WifiManager createService(ContextImpl ctx) throws ServiceNotFoundException {
                IBinder b = ServiceManager.getServiceOrThrow(Context.WIFI_SERVICE);
                IWifiManager service = IWifiManager.Stub.asInterface(b);
                return new WifiManager(ctx.getOuterContext(), service,
                        ConnectivityThread.getInstanceLooper());
            }});
    }

    /**
     * Gets a system service from a given context.
     */
    public static Object getSystemService(ContextImpl ctx, String name) {
        ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
        return fetcher != null ? fetcher.getService(ctx) : null;
    }

    private static <T> void registerService(String serviceName, Class<T> serviceClass,
            ServiceFetcher<T> serviceFetcher) {
        SYSTEM_SERVICE_NAMES.put(serviceClass, serviceName);
        SYSTEM_SERVICE_FETCHERS.put(serviceName, serviceFetcher);
    }

看到这里第二步的方案已经出来了,单例就是 WifiManager 而接口对象就是 WifiManager 中的 mService 对象,只要 Hook 住 mService 就可以了,在 《Android 源码分析实战 - 授权时拦截 QQ 用户名和密码》一文中就是用的这种方案。这里我们再分析一个切入点,我们接着往 ServiceManager.getServiceOrThrow 中看:

    private static HashMap<String, IBinder> sCache = new HashMap<String, IBinder>();

    public static IBinder getService(String name) {
        try {
            IBinder service = sCache.get(name);
            if (service != null) {
                return service;
            } else {
                return Binder.allowBlocking(rawGetService(name));
            }
        } catch (RemoteException e) {
            Log.e(TAG, "error in getService", e);
        }
        return null;
    }

    public static IBinder getServiceOrThrow(String name) throws ServiceNotFoundException {
        final IBinder binder = getService(name);
        if (binder != null) {
            return binder;
        } else {
            throw new ServiceNotFoundException(name);
        }
    }

再往 IWifiManager.Stub.asInterface 中看:

public static IWifiManager asInterface(android.os.IBinder obj)
    {
      if ((obj==null)) {
        return null;
      }
      android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
      if (((iin!=null)&&(iin instanceof IWifiManager))) {
        return ((IWifiManager)iin);
      }
      return new IWifiManager.Stub.Proxy(obj);
    }

看到这里我们就有了第二种方案了,Hook 住 Binder 对象的 queryLocalInterface 方法返回一个代理对象即可。最后一步就是设计实现类了:

public class SystemServiceBinderHooker {
    public interface HookCallback {
        void onServiceMethodInvoke(Method method, Object[] args);

        Object onServiceMethodIntercept(Object receiver, Method method, Object[] args) throws Throwable;
    }

    private String mServiceName;
    private String mServiceClassName;
    private HookCallback mHookCallback;

    public SystemServiceBinderHooker(String serviceName, String serviceClassName, HookCallback hookCallback){
        this.mServiceName = serviceName;
        this.mServiceClassName = serviceClassName;
        this.mHookCallback = hookCallback;
    }

    public boolean hook(){
        try {
            // 1. 先获取 origin 的 IBinder 对象
            Class<?> serviceManagerClass = Class.forName("android.os.ServiceManager");

            Method getServiceMethod = serviceManagerClass.getDeclaredMethod("getService",String.class);

            getServiceMethod.setAccessible(true);

            final IBinder serviceBinder = (IBinder) getServiceMethod.invoke(null,mServiceName);

            // 2. hook 住 serviceBinder 创建代理对象
            IBinder proxyServiceBinder = (IBinder) Proxy.newProxyInstance(serviceManagerClass.getClassLoader(), new Class<?>[]{IBinder.class}, new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    if (TextUtils.equals(method.getName(), "queryLocalInterface")) {
                        return createServiceProxy(serviceBinder);
                    }
                    return method.invoke(serviceBinder, args);
                }
            });

            // 3. 把代理对象塞到 ServiceManager 中的 sCache
            Field sCacheField = serviceManagerClass.getDeclaredField("sCache");
            sCacheField.setAccessible(true);
            Map<String, IBinder> sCache = (Map<String, IBinder>) sCacheField.get(null);
            sCache.put(mServiceName, proxyServiceBinder);

            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    private Object createServiceProxy(IBinder serviceBinder) {
        try {
            // new IWifiManager.Stub.Proxy
            Class<?> serviceProxyClass = Class.forName(mServiceClassName + "$Stub$Proxy");
            Constructor<?> serviceProxyConstructor = serviceProxyClass.getDeclaredConstructor(IBinder.class);
            serviceProxyConstructor.setAccessible(true);
            final Object originServiceProxy = serviceProxyConstructor.newInstance(serviceBinder);

            // hook serviceProxy
            Object serviceProxyHooker = Proxy.newProxyInstance(serviceProxyClass.getClassLoader(), new Class<?>[]{IBinder.class, IInterface.class, Class.forName(mServiceClassName)}, new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    if (mHookCallback != null) {
                        mHookCallback.onServiceMethodInvoke(method, args);
                        Object result = mHookCallback.onServiceMethodIntercept(originServiceProxy, method, args);
                        if (result != null) {
                            return result;
                        }
                    }
                    return method.invoke(originServiceProxy, args);
                }
            });
            return serviceProxyHooker;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
}

视频链接:https://pan.baidu.com/s/164aJyOYlXm-JCOC0_HVBtQ
视频密码:vsfu

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容