宿主启动没有在AndroidManifest中声明的Activity/Service

news2024/11/28 16:32:55

启动没有在AndroidManifest中声明的插件Activity

在前一篇文章中,我们已经可以在宿主中启动一个插件中的Activity了,但该Activity 必须在宿主的AndroidManifest文件中声明,否则会抛出ActivityNotFound异常。
我们需要做的是欺上瞒下的做法,也就是在AMS的startActivity方法中借助在宿主App的AndroidManifest.xml文件中声明的StubActivity,来临时替换掉真正要启动的插件Activity,然后在ActivityThread的Handler的事件中把StubActivity再换回插件Activity。

public class AMSHookHelper {
    public static final String EXTRA_TARGET_INTENT = "extra_target_intent";

    /**
     * Hook AMS
     * 主要完成的操作是  "把真正要启动的Activity临时替换为在AndroidManifest.xml中声明的替身Activity",进而骗过AMS
     */
    public static void hookAMS() throws ClassNotFoundException,
            NoSuchMethodException, InvocationTargetException,
            IllegalAccessException, NoSuchFieldException {

        // 获取AMN的gDefault对象实例
        Object singletonObject = null;
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P){
            singletonObject = RefInvoke.getStaticFieldObject("android.app.ActivityTaskManager","IActivityTaskManagerSingleton");
        }else if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
            singletonObject = RefInvoke.getStaticFieldObject("android.app.ActivityTaskManager","IActivityManagerSingleton");
        }else{
            // 获取AMN的gDefault单例,gDefault是final静态的
            singletonObject = RefInvoke.getStaticFieldObject("android.app.ActivityManagerNative","gDefault");
        }

        // singletonObject 是一个 android.util.Singleton<T>对象; 我们取出这个单例里面的mInstance字段
        Object mInstance = RefInvoke.getFieldObject("android.util.Singleton", singletonObject, "mInstance");

        // 创建一个这个对象的代理对象MockClass1, 然后替换这个字段, 让我们的代理对象帮忙干活
        Class<?> activityManagerInterface;
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.P){
            activityManagerInterface = Class.forName("android.app.IActivityTaskManager");
        }else {
            activityManagerInterface = Class.forName("android.app.IActivityManager");
        }

        Object proxy = Proxy.newProxyInstance(
                Thread.currentThread().getContextClassLoader(),
                new Class[]{ activityManagerInterface },
                new AMSProxy(mInstance));

        RefInvoke.setFieldObject("android.util.Singleton", singletonObject, "mInstance",proxy);
    }

    /**
     * 由于之前我们用替身欺骗了AMS; 现在我们要换回我们真正需要启动的Activity
     * 不然就真的启动替身了, 狸猫换太子...
     * 到最终要启动Activity的时候,会交给ActivityThread 的一个内部类叫做 H 来完成
     * H 会完成这个消息转发; 最终调用它的callback
     */
    public static void hookActivityThread() throws Exception {

        // 先获取到当前的ActivityThread对象
        Object currentActivityThread = RefInvoke.getStaticFieldObject("android.app.ActivityThread", "sCurrentActivityThread");

        // 由于ActivityThread一个进程只有一个,我们获取这个对象的mH
        Handler mH = (Handler) RefInvoke.getFieldObject(currentActivityThread, "mH");

        //把Handler的mCallback字段,替换为new MockClass2(mH)
        RefInvoke.setFieldObject(Handler.class,
                mH, "mCallback", new HandlerProxy(mH));
    }
}

AMSProxy类如下:

class AMSProxy implements InvocationHandler {

    private static final String TAG = "MockClass1";
	private static final String stubPackage = "jianqiang.com.hostapp";
    
    Object mBase;

    public AMSProxy(Object base) {
        mBase = base;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

        Log.e(TAG, method.getName());

        if ("startActivity".equals(method.getName())) {
            // 只拦截这个方法
            // 替换参数, 任你所为;甚至替换原始Activity启动别的Activity偷梁换柱

            // 找到参数里面的第一个Intent 对象
            Intent raw;
            int index = 0;

            for (int i = 0; i < args.length; i++) {
                if (args[i] instanceof Intent) {
                    index = i;
                    break;
                }
            }
            raw = (Intent) args[index];

            Intent newIntent = new Intent();

            // 这里我们把启动的Activity临时替换为 StubActivity
            ComponentName componentName = new ComponentName(stubPackage, StubActivity.class.getName());
            newIntent.setComponent(componentName);

            // 把我们原始要启动的TargetActivity先存起来
            newIntent.putExtra(AMSHookHelper.EXTRA_TARGET_INTENT, raw);

            // 替换掉Intent, 达到欺骗AMS的目的
            args[index] = newIntent;

            Log.d(TAG, "hook success");
            return method.invoke(mBase, args);
        }

        return method.invoke(mBase, args);
    }
}

HandlerProxy类如下:

class HandlerProxy implements Handler.Callback {

    Handler mBase;

    public HandlerProxy(Handler base) {
        mBase = base;
    }

    @Override
    public boolean handleMessage(Message msg) {

        switch (msg.what) {
            // ActivityThread里面 "LAUNCH_ACTIVITY" 这个字段的值是100
            // 本来使用反射的方式获取最好, 这里为了简便直接使用硬编码
            case 100:   //for API 28以下
                handleLaunchActivity(msg);
                break;
            case 159: // //for API 28以上
                handleActivity(msg);
                break;
        }

        mBase.handleMessage(msg);
        return true;
    }

   private void handleActivity(Message msg) {
        // 这里简单起见,直接取出TargetActivity;
        Object obj = msg.obj; // ClientTransaction对象

        List<Object> mActivityCallbacks = (List<Object>) RefInvoke.getFieldObject(obj, "mActivityCallbacks");
        if(mActivityCallbacks.size() > 0) {
            String className = "android.app.servertransaction.LaunchActivityItem";
            if(mActivityCallbacks.get(0).getClass().getCanonicalName().equals(className)) {
                // 找到LaunchActivityItem
                Object object = mActivityCallbacks.get(0);
                // 找到传递过来的intent
                Intent intent = (Intent) RefInvoke.getFieldObject(object, "mIntent");
                Intent target = intent.getParcelableExtra(AMSHookHelper.EXTRA_TARGET_INTENT);
                intent.setComponent(target.getComponent());
            }
        }
    }

    private void handleLaunchActivity(Message msg) {
        // 取出TargetActivity
        Object obj = msg.obj; // ActivityClientRecord

        // 把替身恢复成真身,拿到 ActivityClientRecord 中的intent
        Intent raw = (Intent) RefInvoke.getFieldObject(obj,"intent");
        Intent target = raw.getParcelableExtra(AMSHookHelper.EXTRA_TARGET_INTENT);
        raw.setComponent(target.getComponent());
    }

}

这样就可以正常启动一个没有在AndroidManifest.xml中声明的Activity了。

启动没有在AndroidManifest中声明的插件Service

ActivityThread最终是通过Instrumentation启动一个Activity的。而ActivityThread启动Service并不借助于Instrumentation,而是直接把Service反射出来就启动了。Instrumentation只给Activity提供服务

startService的解决方案

1. 在宿主App中声明几个占位Service,数量和插件中的Service相同
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="jianqiang.com.hostapp">
	...
    <application
        android:name=".MyApplication"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme">

        <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>
        </activity>
			...

        <service android:name="jianqiang.com.hostapp.StubService1"/>
        <service android:name="jianqiang.com.hostapp.StubService2"/>
    </application>

</manifest>
2. 在插件的Assets目录中创建一个plugin_config.json配置文件,让宿主的占位Service和插件中的Service一一对应
{
  "plugins": [
    {
      "PluginService": "jianqiang.com.plugin1.MyService1",
      "StubService": "jianqiang.com.hostapp.StubService1"
    },
    {
      "PluginService": "jianqiang.com.plugin1.MyService2",
      "StubService": "jianqiang.com.hostapp.StubService2"
    }
  ]
}

定义一个方法来解析配置文件:

 /**
     * 读取zip文件中某个文件为字符串,参看自Zeus的PluginUtil
     * @param zipFile     压缩文件
     * @param fileNameReg 需要获取的文件名
     * @return 获取的字符串
     */
    public static String readZipFileString(String zipFile,String fileNameReg){
        final int BUF_SIZE = 8192;

        String result = null;
        byte[] buffer = new byte[BUF_SIZE];
        InputStream in = null;
        ZipInputStream zipIn = null;
        ByteArrayOutputStream bos = null;
        try {
            File file = new File(zipFile);
            if (!file.exists()) return null;
            in = new FileInputStream(file);
            zipIn = new ZipInputStream(in);
            ZipEntry entry;
            while (null != (entry = zipIn.getNextEntry())) {
                String zipName = entry.getName();
                if (zipName.equals(fileNameReg)) {
                    int bytes;
                    int count = 0;
                    bos = new ByteArrayOutputStream();

                    while ((bytes = zipIn.read(buffer, 0, BUF_SIZE)) != -1) {
                        bos.write(buffer, 0, bytes);
                        count += bytes;
                    }
                    if (count > 0) {
                        result = bos.toString();
                        break;
                    }
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                closeSilently(in);
                closeSilently(zipIn);
                closeSilently(bos);
            } catch (Exception e2) {
                e2.printStackTrace();
            }
        }
        return result;
    }
3. 把插件和宿主的dex合并
/**
 * 由于应用程序使用的ClassLoader为PathClassLoader
 * 最终继承自 BaseDexClassLoader
 * 查看源码得知,这个BaseDexClassLoader加载代码根据一个叫做
 * dexElements的数组进行, 因此我们把包含代码的dex文件插入这个数组
 * 系统的classLoader就能帮助我们找到这个类
 *
 * 这个类用来进行对于BaseDexClassLoader的Hook
 * 类名太长, 不要吐槽.
 * @author weishu
 * @date 16/3/28
 */
public final class BaseDexClassLoaderHookHelper {

    public static void patchClassLoader(ClassLoader cl, File apkFile, File optDexFile)
            throws IllegalAccessException, NoSuchMethodException, IOException, InvocationTargetException, InstantiationException, NoSuchFieldException {
        // 获取 BaseDexClassLoader : pathList
        Object pathListObj = RefInvoke.getFieldObject(DexClassLoader.class.getSuperclass(), cl, "pathList");

        // 获取 PathList: Element[] dexElements
        Object[] dexElements = (Object[]) RefInvoke.getFieldObject(pathListObj, "dexElements");

        // Element 类型
        Class<?> elementClass = dexElements.getClass().getComponentType();

        // 创建一个数组, 用来替换原始的数组
        Object[] newElements = (Object[]) Array.newInstance(elementClass, dexElements.length + 1);

        // 构造插件Element(File file, boolean isDirectory, File zip, DexFile dexFile) 这个构造函数
        Class[] p1 = {File.class, boolean.class, File.class, DexFile.class};
        Object[] v1 = {apkFile, false, apkFile, DexFile.loadDex(apkFile.getCanonicalPath(), optDexFile.getAbsolutePath(), 0)};
        Object o = RefInvoke.createObject(elementClass, p1, v1);

        Object[] toAddElementArray = new Object[] { o };
        // 把原始的elements复制进去
        System.arraycopy(dexElements, 0, newElements, 0, dexElements.length);
        // 插件的那个element复制进去
        System.arraycopy(toAddElementArray, 0, newElements, dexElements.length, toAddElementArray.length);

        // 替换
        RefInvoke.setFieldObject(pathListObj, "dexElements", newElements);
    }
}
4. 采用“欺上瞒下”的方法
public class AMSHookHelperForService {
    public static final String EXTRA_TARGET_INTENT = "extra_target_intent";

    /**
     * Hook AMS
     * 主要完成的操作是  "把真正要启动的Activity临时替换为在AndroidManifest.xml中声明的替身Activity",进而骗过AMS
     */
    public static void hookAMS() throws ClassNotFoundException,
            NoSuchMethodException, InvocationTargetException,
            IllegalAccessException, NoSuchFieldException {

        // 获取AMN的gDefault对象实例
        Object singletonObject = null;
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
            singletonObject = RefInvoke.getStaticFieldObject("android.app.ActivityManager","IActivityManagerSingleton");
        }else{
            // 获取AMN的gDefault单例,gDefault是final静态的
            singletonObject = RefInvoke.getStaticFieldObject("android.app.ActivityManagerNative","gDefault");
        }

        // singletonObject 是一个 android.util.Singleton<T>对象; 我们取出这个单例里面的mInstance字段
        Object mInstance = RefInvoke.getFieldObject("android.util.Singleton", singletonObject, "mInstance");

        // 创建一个这个对象的代理对象MockClass1, 然后替换这个字段, 让我们的代理对象帮忙干活
        Class<?> activityManagerInterface;
        activityManagerInterface = Class.forName("android.app.IActivityManager");

        Object proxy = Proxy.newProxyInstance(
                Thread.currentThread().getContextClassLoader(),
                new Class[]{ activityManagerInterface },
                new AMSProxy(mInstance));

        RefInvoke.setFieldObject("android.util.Singleton", singletonObject, "mInstance",proxy);
    }

    /**
     * 由于之前我们用替身欺骗了AMS; 现在我们要换回我们真正需要启动的Activity
     * 不然就真的启动替身了, 狸猫换太子...
     * 到最终要启动Activity的时候,会交给ActivityThread 的一个内部类叫做 H 来完成
     * H 会完成这个消息转发; 最终调用它的callback
     */
    public static void hookActivityThread() throws Exception {

        // 先获取到当前的ActivityThread对象
        Object currentActivityThread = RefInvoke.getStaticFieldObject("android.app.ActivityThread", "sCurrentActivityThread");

        // 由于ActivityThread一个进程只有一个,我们获取这个对象的mH
        Handler mH = (Handler) RefInvoke.getFieldObject(currentActivityThread, "mH");

        //把Handler的mCallback字段,替换为new HandlerProxy(mH)
        RefInvoke.setFieldObject(Handler.class,
                mH, "mCallback", new HandlerProxy(mH));
    }
}

和Activity不同的是,startService被委托给了ActivityManagerService的startService方法:

在AMSProxy文件中添加startService 和 stopService 的捕获:

public class AMSProxy implements InvocationHandler {
    private static final String TAG = "AMSProxy";
    private static final String stubPackage = "jianqiang.com.hostapp";

    Object mBase;

    public AMSProxy(Object mBase) {
        this.mBase = mBase;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Log.e(TAG, method.getName());

        if ("startActivity".equals(method.getName())) {
            // 只拦截这个方法
            // 替换参数, 任你所为;甚至替换原始Activity启动别的Activity偷梁换柱

            // 找到参数里面的第一个Intent 对象
            Intent raw;
            int index = 0;

            for (int i = 0; i < args.length; i++) {
                if (args[i] instanceof Intent) {
                    index = i;
                    break;
                }
            }
            raw = (Intent) args[index];

            Intent newIntent = new Intent();

            // 替身Activity的包名, 也就是我们自己的包名
            String stubPackage = "jianqiang.com.hostapp";

            // 这里我们把启动的Activity临时替换为 StubActivity
            ComponentName componentName = new ComponentName(stubPackage, StubActivity.class.getName());
            newIntent.setComponent(componentName);

            // 把我们原始要启动的TargetActivity先存起来
            newIntent.putExtra(AMSHookHelper.EXTRA_TARGET_INTENT, raw);

            // 替换掉Intent, 达到欺骗AMS的目的
            args[index] = newIntent;

            Log.d(TAG, "hook success");
            return method.invoke(mBase, args);
        }else if("startService".equals(method.getName())){
            // hook startService

            // 找到参数里面的第一个Intent 对象
            int index = 0;
            for (int i = 0; i < args.length; i++) {
                if (args[i] instanceof Intent) {
                    index = i;
                    break;
                }
            }
            // 拿到启动Service时的Intent
            Intent rawIntent = (Intent) args[index];
            // jianqiang.com.plugin1.MyService1
            String rawServiceName = rawIntent.getComponent().getClassName();
            // 拿到插件Service对应的Service  jianqiang.com.hostapp.StubService1
            String stubServiceName = MyApplication.pluginServices.get(rawServiceName);
            // replace Plugin Service of StubService
            ComponentName componentName = new ComponentName(stubPackage, stubServiceName);
            Intent newIntent = new Intent();
            newIntent.setComponent(componentName);
            // Replace Intent, cheat AMS
            args[index] = newIntent;

            Log.d(TAG, "hook success");
            return method.invoke(mBase, args);
        }else if("stopService".equals(method.getName())){
            // 找到参数里面的第一个Intent 对象
            int index = 0;
            for (int i = 0; i < args.length; i++) {
                if (args[i] instanceof Intent) {
                    index = i;
                    break;
                }
            }

            //get StubService form UPFApplication.pluginServices
            Intent rawIntent = (Intent) args[index];
            String rawServiceName = rawIntent.getComponent().getClassName();
            String stubServiceName = MyApplication.pluginServices.get(rawServiceName);
            // replace Plugin Service of StubService
            ComponentName componentName = new ComponentName(stubPackage, stubServiceName);
            Intent newIntent = new Intent();
            newIntent.setComponent(componentName);

            // Replace Intent, cheat AMS
            args[index] = newIntent;

            Log.d(TAG, "hook success");
            return method.invoke(mBase, args);
        } 
        return method.invoke(mBase, args);
    }
}

AMS被欺骗后,它原本会通知APP启动StubService,而我们要Hook掉ActivityThread的mH对象的mCallback对象,仍然截获它的handleMessage方法,只不过这次截取的是CREATE_SERVICE(value=114)分支,这个分支执行ActivityThread中的handleCreateService方法。我们看源码的实现:

public void handleMessage(Message msg) {
    switch (msg.what) {
       ...

        case CREATE_SERVICE:
            if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {
                Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER,
                        ("serviceCreate: " + String.valueOf(msg.obj)));
            }
            handleCreateService((CreateServiceData)msg.obj);
            Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
            break;

        ... 
    }
}
private void handleCreateService(CreateServiceData data) {
	LoadedApk packageInfo = getPackageInfoNoCheck(
            data.info.applicationInfo, data.compatInfo);
    Service service = null;
    try {
        //创建service的context
        ContextImpl context = ContextImpl.createAppContext(this, packageInfo);
        //创建Application
        Application app = packageInfo.makeApplication(false, mInstrumentation);
        //获取类加载器
        java.lang.ClassLoader cl = packageInfo.getClassLoader();
        //加载service实例
        service = packageInfo.getAppFactory()
                .instantiateService(cl, data.info.name, data.intent);
                ...
                //初始化service
        service.attach(context, this, data.info.name, data.token, app,
                ActivityManager.getService());
        //调用service的onCreate方法
        service.onCreate();
     } catch (Exception e) {
        if (!mInstrumentation.onException(service, e)) {
            throw new RuntimeException(
                "Unable to create service " + data.info.name
                + ": " + e.toString(), e);
        }
    }
}

在handleCreateService 方法中,可以通过CreateServiceData来获取要启动的Service的信息。只需要将data.info.name 的值Hook为插件Service即可。

public class HandlerProxy implements Handler.Callback {
    Handler mBase;

    public HandlerProxy(Handler mBase) {
        this.mBase = mBase;
    }

    @Override
    public boolean handleMessage(@NonNull Message msg) {
        switch (msg.what){
            case 100: // //for API 28以下
                handleLaunchActivity(msg);
                break;
            case 159: // //for API 28以上
                handleActivity(msg);
                break;
            case 114: // CREATE_SERVICE
                handleCreateService(msg);
                break;
        }
        mBase.handleMessage(msg);
        return true;
    }

    private void handleCreateService(Message msg) {
        Object obj = msg.obj;
        ServiceInfo serviceInfo = (ServiceInfo) RefInvoke.getFieldObject(obj, "info");

        String realServiceName = null;

        for (String key : MyApplication.pluginServices.keySet()) {
            String value = MyApplication.pluginServices.get(key);
            if(value.equals(serviceInfo.name)) {
                realServiceName = key;
                break;
            }
        }

        if(TextUtils.isEmpty(realServiceName)){
            // 只有在plugin_config中注册的才不会为空
            return;
        }

        serviceInfo.name = realServiceName;
    }

    private void handleActivity(Message msg) {
        // 这里简单起见,直接取出TargetActivity;
        Object obj = msg.obj; // ClientTransaction对象

        List<Object> mActivityCallbacks = (List<Object>) RefInvoke.getFieldObject(obj, "mActivityCallbacks");
        if(mActivityCallbacks.size() > 0) {
            String className = "android.app.servertransaction.LaunchActivityItem";
            if(mActivityCallbacks.get(0).getClass().getCanonicalName().equals(className)) {
                // 找到LaunchActivityItem
                Object object = mActivityCallbacks.get(0);
                // 找到传递过来的intent
                Intent intent = (Intent) RefInvoke.getFieldObject(object, "mIntent");
                Intent target = intent.getParcelableExtra(AMSHookHelper.EXTRA_TARGET_INTENT);
                intent.setComponent(target.getComponent());
            }
        }
    }

    private void handleLaunchActivity(Message msg) {
        // 取出TargetActivity
        Object obj = msg.obj; // ActivityClientRecord

        // 把替身恢复成真身,拿到 ActivityClientRecord 中的intent
        Intent raw = (Intent) RefInvoke.getFieldObject(obj,"intent");
        Intent target = raw.getParcelableExtra(AMSHookHelper.EXTRA_TARGET_INTENT);
        raw.setComponent(target.getComponent());
    }
}

如此就完成了支持startService。

在宿主中调用

 public void startUnRegisterService1InPlugin1(View view) {
        try {

            Intent intent = new Intent();
            intent.setComponent(
                    new ComponentName("jianqiang.com.plugin1",
                            "jianqiang.com.plugin1.MyService1"));
            startService(intent);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void stopUnRegisterService1InPlugin1(View view) {
        try {
            Intent intent = new Intent();
            intent.setComponent(
                    new ComponentName("jianqiang.com.plugin1",
                            "jianqiang.com.plugin1.MyService1"));
            stopService(intent);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

bindService的解决方案

只需要在AMSProxy中增加一个分支,在bindService时欺骗AMS就行了:

else if("bindIsolatedService".equals(method.getName())){
            // 找到参数里面的第一个Intent 对象
            int index = 0;
            for (int i = 0; i < args.length; i++) {
                if (args[i] instanceof Intent) {
                    index = i;
                    break;
                }
            }

            Intent rawIntent = (Intent) args[index];
            String rawServiceName = rawIntent.getComponent().getClassName();
            String stubServiceName = MyApplication.pluginServices.get(rawServiceName);


            // replace Plugin Service of StubService
            ComponentName componentName = new ComponentName(stubPackage, stubServiceName);
            Intent newIntent = new Intent();
            newIntent.setComponent(componentName);

            // Replace Intent, cheat AMS
            args[index] = newIntent;

            Log.d(TAG, "hook success");
            return method.invoke(mBase, args);
        }

接下来就可以在宿主中调用:

public void bindUnRegisterService1InPlugin1(View view) {
        try {

            Intent intent = new Intent();
            intent.setComponent(
                    new ComponentName("jianqiang.com.plugin1",
                            "jianqiang.com.plugin1.MyService2"));
            bindService(intent, serviceConnection, Service.BIND_AUTO_CREATE);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    ServiceConnection serviceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            Log.i("TAG","onServiceConnected   " + name);
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            Log.i("TAG","onServiceConnected   " + name);
        }
    };
    public void unbindUnRegisterService1InPlugin1(View view) {
        try {
            if(null != serviceConnection){
                unbindService(serviceConnection);
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

问1:为什么不在unbind的时候欺骗AMS?

因为unbind语法是unbindService(conn),AMS会根据conn来找到对应的Service,所以我们不需要把MyService2替换为StubService2

问2:为什么在MockClass2中不需要吧StubService2切换回MyService2?

因为bindService是先走handleCreateService再走handleBindService方法。在handleCreateService方法中已经将StubService2切换回MyService2了,所以后面不需要切换了。

在这里插入图片描述
源码

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/572789.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

kafka题集 - kafka 命令行操作面试题总结

文章目录 01. kafka 主题命令行操作02. kafka 生产者命令行操作03. kafka 消费者命令行操作04. Kafka 命令行工具有哪些常用的命令&#xff1f;05. 如何创建一个 Kafka 主题&#xff1f;06. 如何列出 Kafka 中所有的主题&#xff1f;07. 如何向 Kafka 主题发送消息&#xff1f;…

六种基本网络拓扑结构详解

目录 1、总线型网络拓扑结构 2、星型网络拓扑结构 3、环形网络拓扑结构 4、树型网络拓扑结构 5、网状网络拓扑结构 6、混合网络型拓扑结构 常见的网络拓扑结构有以下6种&#xff1a;1.总线型网络拓扑结构&#xff1b;2.星型网络拓扑结构&#xff1b;3.环形网络拓扑结构&a…

Oracle SQL 性能优化

向量I/O 回表 SQL 不一样&#xff0c;plan 一样的 除了统计信息&#xff0c;session 参数导致COST不对 历史执行计划 filter 不同于nest loop 会distinct 之类 放进PGA 不再是SGA中块访问了吧 sql profile fzw rman target / 慢的原因 降低驱动表的row source集 指定nl表的驱动…

EasyRecovery16绿色版安装下载及使用教程

如果你已经在下载了PC版本的EasyRecovery&#xff0c;那么该如何安装EasyRecovery呢&#xff1f;现在就呈上EasyRecovery教程&#xff0c;以便顺利完成安装。EasyRecovery不仅能够恢复多种类型的数据&#xff0c;更能够适用于不同媒体介质&#xff0c;其中包括计算机&#xff0…

Kylin从入门到精通以及案例实操系列

1、Kylin 基础知识 1.1、了解 Kylin 的基本概念、原理和架构 1.1.1、Kylin 定义 Apache Kylin是一个开源的分布式分析引擎&#xff0c;提供Hadoop/Spark之上的SQL查询接口及多维分析&#xff08;OLAP&#xff09;能力以支持超大规模数据&#xff0c;最初由eBay Inc开发并贡献…

书评 | 《新程序员005:开源深度指南 新金融背后的科技力量》

目录 书评 | 《新程序员005&#xff1a;开源深度指南 & 新金融背后的科技力量》 内容介绍 书籍优点 书评 书评 | 《新程序员005&#xff1a;开源深度指南 & 新金融背后的科技力量》 内容介绍 《新程序员005&#xff1a;开源深度指南&amp;新金融背后的科技力量》特…

八、Spring Cloud Alibaba-seata分布式事务

一、引言 1、事务简介 事务(Transaction)是访问并可能更新数据中各种数据项的一个程序执行单元(unit&#xff09;。在关系数据库中&#xff0c;一个事务由一组SQL语向组成。事务应该具有4个属性:原子性、一致性、隔离性、持久性。这四个属性通常称为ACID特性。 原子性(atomic…

用Java 的锁机制实现多线程售票案例

本文首发自「慕课网」&#xff0c;想了解更多IT干货内容&#xff0c;程序员圈内热闻&#xff0c;欢迎关注"慕课网"及“慕课网公众号”&#xff01; 作者&#xff1a;王军伟Tech | 慕课网讲师 1. 前言 本文内容主要是使用 Java 的锁机制对多线程售票案例进行实现。售…

logback高级特性使用

一、业务需求 日志级别的分类 日志的级别分为&#xff1a; trace&#xff1a;微量&#xff0c;少许的意思&#xff0c;级别最低info&#xff1a;普通的打印信息debug&#xff1a;需要调试时候的关键信息打印warn&#xff1a;警告&#xff0c;不影响使⽤&#xff0c;但需要注…

windows系统python3.7版本pyspider安装

环境&#xff1a;很多的python版本都尝试过安装pyspider&#xff0c;网上多数让python3.6安装&#xff0c;说是这个环境是最佳的环境&#xff0c;测试安装最方便快捷&#xff0c;但是一直报result_worker starting…&#xff01;&#xff01;&#xff01;&#xff0c;卡死在界面…

软件测试技术才是王道,43岁照样拿到年薪70W+,太强了...

最近挺丧的&#xff0c; 可能是之前弦绷的有点紧&#xff0c;现在有点受不了了。 所以突然就泄了气&#xff0c;每天忙完工作的事后就躺在家里打游戏。其实感觉每年都有一段时间是这样丧的。所以我自己其实并不是特别努力的类型&#xff0c;我没办法一直绷着弦的去卷&#xff0…

0基础想入门互联网选择什么好?

互联网岗位划分 研发&#xff1a;技术岗&#xff0c;需要有相关的专业知识。 测试&#xff1a;技术岗&#xff0c;通过相关的程序查找产品中相应的bug。 设计&#xff1a;需要美术素养。 产品经理&#xff1a;设计制定产品的原型&#xff0c;制定每个功能的需求以及输出相应…

论文解读 | IROS 2022:MV6D:在RGB-D图像上使用深度逐点投票网络进行多视角6D姿态估计

原创 | 文 BFT机器人 01 研究背景 在计算机视觉领域&#xff0c;6D姿态估计是一种重要的任务&#xff0c;用于确定物体在3D空间中的位置和方向。它在许多应用领域具有广泛的应用&#xff0c;如机器人操作、虚拟现实、增强现实、物体跟踪等。 然而&#xff0c;传统的6D姿态估计方…

Jmeter实现分布式并发

Jmeter实现分布式并发&#xff0c;即使用远程机执行用例。 环境&#xff1a; VMware Fusion Windows系统是win7。 操作过程 1、Master在jmeter.properties添加remote_hosts 2、Slave在jmeter.properties添加server_port 同时把remote_hosts修改为和主机&#xff08;Master…

超100篇! VAD论文梳理汇总!

GitHub的一位博主整理了上百篇语音活动检测&#xff08;VAD&#xff09;的论文&#xff0c;按照其中使用的特征方法以及适用的环境进行了分类整理&#xff0c;时间跨度为从198*年至2019年。此外&#xff0c;还提供了几个VAD代码&#xff0c;它们的性能表现较好。需要的同学可以…

我的创作纪念日---[需要更开阔的视野!]

文章目录 头绪收获日常 憧憬英语人工智能 希望 头绪 工作很长时间之后&#xff0c;才发现知识的根本&#xff0c;还是在于积累。俗话说好记性不如烂笔头。不管是特定产品相关的知识还是系统类的知识&#xff0c;又或者是语言类的知识&#xff0c;都有很多知识点需要积累。有了…

不会数据分析?无从下手?一文帮你打开数据分析思路

掌握了很多数据分析工具和技能&#xff0c;却依然做不好数据分析。 面对具体的业务问题&#xff0c;我们还是容易两眼一抹黑&#xff1f;除了数据和专业之外&#xff0c;还需要一定的方法论支撑。 文章有点长&#xff08;误区解释方法论分享&#xff09;但干货满满&#xff0c…

药用辅料数据查询网站系统-药品辅料数据

药用辅料是指在制药过程中&#xff0c;用于增加药品稳定性、改善口感、提高吸收率等功效的辅助材料。药用辅料的种类繁多&#xff0c;不同的药品需要使用不同的辅料&#xff0c;因此对于药企来说&#xff0c;了解并选用适合自己的药用辅料显得尤为重要。本文将介绍如何利用药用…

jvm之对象大小分析

写在前面 本文看下计算对象大小相关内容。 1&#xff1a;基础内容 1.1&#xff1a;对象的结构 一个对象由对象头和对象体组成&#xff0c;其中对象头包含如下内容&#xff1a; 标记字&#xff08;mark word&#xff09;&#xff1a;存放GC年龄信息&#xff0c;对象锁信息等…

Hightopo 使用心得(1)- 基本概念

Hightopo 公司 3D 可视化产品有对应的官方手册。但是这些手册内容比较多。对于想学习的新同学来说可能相对比较繁琐。这里本人根据个人使用经验做了一些总结。希望对读者有所帮助。 官方手册地址&#xff1a;Structure (hightopo.com) 本文会提到一些前端开发的概念&#xff…