Android插件化实现动态加载Activity笔记

Activity的启动流程

Activity的启动是一个很复杂的过程,涉及的类也非常多,这是一张启动UML流程图:
在这里插入图片描述

简化一下大致就是:①startActivity > ②系统获取启动信息 > ③校验Manifast > ④创建Activity > ⑤调用生命周期。

如果我们想要动态加载的Activity位于一个独立的APK(或.jar、.aar文件)中,那么,在主工程的Manifast中提前注册好想要启动的Activity编译器都会给我们报错,显然不是正确的做法。

可选择的正确思路应该是:

  1. ①startActivity时中传入包含启动目标TargetActivityIntent,无需在主工程的Manifast文件中申明。
  2. 在主工程中创建一个空的ProxyActivity并注册在Manifast中,在②系统获取启动信息(Intent)前,将Intent中包含的TargetActivity替换为ProxyActivity,使得系统拿到的是在Manifast中注册过的ProxyActivity,以通过第③步中的系统校验
  3. 然后在系统④创建Activity之前将Intent中包含启动信息ProxyActivity还原回TargetActivity

寻找Hook锚点

我们要通过系统的校验,需要在系统获取Intent信息之前将他替换掉。从 Activity的启动流程 中我们得知ActivityStarter是用于确定intentflags 应如何转换为 activity的类,所以替换锚点应从启动流程进入ActivityStarter类之前的ActivityTaskManagerService(AMS)类中找。

较为理想的Hook锚点是Instrumentation#execStartActivityActivityTaskManager#startActivity

public void startActivityForResult(@RequiresPermission Intent intent, int requestCode,
        @Nullable Bundle options) {
    >>>
	Instrumentation.ActivityResult ar =
		mInstrumentation.execStartActivity(
			this, mMainThread.getApplicationThread(), mToken, this,
			intent, requestCode, options);
	>>>
}

public ActivityResult execStartActivity(
		Context who, IBinder contextThread, IBinder token, Activity target,
		Intent intent, int requestCode, Bundle options) {
	>>>
	int result = ActivityTaskManager.getService()
		.startActivity(whoThread, who.getBasePackageName(), intent,
				intent.resolveTypeIfNeeded(who.getContentResolver()),
				token, target != null ? target.mEmbeddedID : null,
				requestCode, 0, null, options);
	checkStartActivityResult(result, intent);//Activity是否在Manifast中注册过,就是在这个方法里校验的。
	>>>
}

我们看一下execStartActivity方法的实现,发现在不同的SDK版本中谷歌已经对他进行了三次改动:

  • Android8.0之前的版本中通过ActivityManagerNative.getDefault获取ActivityManagerService的实例来调用其startActivity方法。
  • Android10之前的版本是通过ActivityManager.getService获取ActivityManagerService的实例来调用其startActivity方法。
  • Android10开始的版本是通过ActivityTaskManager.getService获取ActivityTaskManagerService的实例来调用其startActivity方法。

不同版本之间,获取AMS实例的类是不同的。从版本兼容性上来讲,把ActivityTaskManager作为Hook目标并不是很理想。我们再把目光转向Instrumentation

Instrumentation 用于实现应用程序插装代码的基本类,主要负责ActivityApplication的创建和生命周期调用。在运行时,这个类将在任何应用程序代码之前被实例化,从而允许我们监视系统与应用程序的所有交互。

从类的职能上来讲execStartActivity是可以选做Hook目标的。而且,此类并没有因为版本的差异而改变其创建方式。所以,替换ProxyActivity选择Hook Instrumentation#execStartActivity的方式 要更合适一些。

替换的锚点找到了,那么还原的呢?我们的最终目的是要系统在创建启动Activity的时候使用我们指定的Intent,所以要从Activity创建之前去寻找。

还是从 Activity的启动流程 来看。ActivityThread在收到Handler发送的启动Activity的消息后会在ActivityThread#performLaunchActivity中处理:

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
	>>>
	Activity activity = null;
	try {
		java.lang.ClassLoader cl = appContext.getClassLoader();
		activity = mInstrumentation.newActivity(
				cl, component.getClassName(), r.intent);
		StrictMode.incrementExpectedActivityCount(activity.getClass());
		r.intent.setExtrasClassLoader(cl);
		r.intent.prepareToEnterProcess();
		if (r.state != null) {
			r.state.setClassLoader(cl);
		}
	} catch (Exception e) {
		>>>
	}
	>>>
	if (r.isPersistable()) {
		mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
	} else {
		mInstrumentation.callActivityOnCreate(activity, r.state);
	}
	>>>
}

Instrumentation#newActivity:

public Activity newActivity(ClassLoader cl, String className,
		Intent intent)
		throws InstantiationException, IllegalAccessException,
		ClassNotFoundException {
	String pkg = intent != null && intent.getComponent() != null
			? intent.getComponent().getPackageName() : null;
	return getFactory(pkg).instantiateActivity(cl, className, intent);
}

public @NonNull Activity instantiateActivity(@NonNull ClassLoader cl, @NonNull String className,
		@Nullable Intent intent)
		throws InstantiationException, IllegalAccessException, ClassNotFoundException {
	return (Activity) cl.loadClass(className).newInstance();
}

Activity的实例是在类Instrumentation中开始创建的。因为替换点我们选择了 Instrumentation类的execStartActivity方法。所以,Instrumentation 类的newActivity可以作为还原点,这样我们只需要Hook Instrumentation一个类,可以避免过多的Hook操作,后续SDK版本中谷歌如果对API做了改动,我们也只需要主要关注这一个类。

替换目标Activity

Hook操作会涉及到一系列反射操作,我们可以封装一个反射的工具类ReflectUtil:

public class ReflectUtil{

    public static Object getField(Class clazz, Object target, String name) throws Exception {
        Field field = getField(clazz, name);
        return field.get(target);
    }

    public static Field getField(Class clazz, String name) throws Exception {
        Field field = findField(clazz, name);
        field.setAccessible(true);
        return field;
    }

    public static void setField(Class clazz, Object target, String name, Object value) throws Exception {
        Field field = getField(clazz, name);
        field.set(target, value);
    }

    private static Field findField(@NonNull Class clazz, @NonNull String name) throws NoSuchFieldException {
        try {
            return clazz.getField(name);
        } catch (NoSuchFieldException e) {
            for (Class<?> cls = clazz; cls != null; cls = cls.getSuperclass()) {
                try {
                    return cls.getDeclaredField(name);
                } catch (NoSuchFieldException ex) {
                    // Ignored
                }
            }
            throw e;
        }
    }
}

创建一个继承自Instrumentation的类,重写其execStartActivity方法(该方法被标注为Hide,不对开发者开放,我们创建一个同名同参的方法)。InstrumentationProxy作为代理类,当系统需要调用InstrumentationexecStartActivity方法时,会直接调用到我们的代理类中对应的方法。在完成了代理Activity的替换工作后,我们仍然需要系统按照正常流程走下去。所以,必须调用InstrumentationexecStartActivity方法。

public class InstrumentationProxy extends Instrumentation {

    private Instrumentation mInstrumentation;

    public InstrumentationProxy(Instrumentation mInstrumentation) {
        this.mInstrumentation = mInstrumentation;
    }

    public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {
        List<ResolveInfo> infoList = who.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_ALL);
        Intent mIntent;
        // 如果目标intent中关联的Activity没有注册过,则为其注入代理类
        if (infoList.size() == 0) {
            mIntent = new Intent(who, ProxyActivity.class);
            mIntent.putExtra(HookHelper.TARGET_INTENT, intent);
        }else {
            mIntent = intent;
        }
        try {
            @SuppressLint("DiscouragedPrivateApi")
            Method execMethod = mInstrumentation.getClass().getDeclaredMethod("execStartActivity",
                    Context.class, IBinder.class, IBinder.class, Activity.class, Intent.class, int.class, Bundle.class);
            Object object = execMethod.invoke(mInstrumentation, who, contextThread, token,
                    target, mIntent, requestCode, options);
            if (object != null) {
                return (ActivityResult) object;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

然后通过反射把InstrumentationProxy 类实例注入到ActivityThread中:

public static void hookInstrumentation(Context activity) throws Exception {
	Class<?> contextImplClass = Class.forName("android.app.ContextImpl");
	Object mMainThread = ReflectUtil.getField(contextImplClass, activity, "mMainThread");
	Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
	Instrumentation mInstrumentation = (Instrumentation)ReflectUtil.getField(activityThreadClass, mMainThread, "mInstrumentation");
	ReflectUtil.setField(activityThreadClass, mMainThread, "mInstrumentation", new InstrumentationProxy(mInstrumentation));
}

做完这一步,运行代码,当我们调用startActivity时就会发现代码将会执行到我们的静态代理类中的execStartActivity方法,并且成功完成了代理Activity的替换。

还原目标Activity

还原的hook点选定在Instrumentation#newActivity方法:

public Activity newActivity(ClassLoader cl, String className,
		Intent intent)
		throws InstantiationException, IllegalAccessException,
		ClassNotFoundException {
	String pkg = intent != null && intent.getComponent() != null
			? intent.getComponent().getPackageName() : null;
	return getFactory(pkg).instantiateActivity(cl, className, intent);
}

还是InstrumentationProxy类,重写InstrumentationnewActivity方法:

public class InstrumentationProxy extends Instrumentation {

    private Instrumentation mInstrumentation;

    public InstrumentationProxy(Instrumentation mInstrumentation) {
        this.mInstrumentation = mInstrumentation;
    }

    @Override
    public Activity newActivity(ClassLoader cl, String className, Intent intent) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        if (intent != null) {
            Intent targetIntent = intent.getParcelableExtra(HookHelper.TARGET_INTENT);
            if (targetIntent != null && targetIntent.getComponent() != null) {
                ComponentName componentName = targetIntent.getComponent();
                return mInstrumentation.newActivity(cl, componentName.getClassName(), targetIntent);
            }
        }
        return super.newActivity(cl, className, intent);
    }

}

注意,第15行不能直接使用super调用newActivity,否则会抛出如下异常:Uninitialized ActivityThread
在这里插入图片描述

因为我们在注入代理实例时只是直接new了一个InstrumentationProxy,在实际的系统流程中Instrumentation还包含一些初始化工作(initbasicInit方法),我们的代理类只做Intent中的activity信息的替换,其他的任何操作仍然需要系统自己的Instrumentation实例对象来完成。

接下来在Application中注入代理:

@Override
protected void attachBaseContext(Context base) {
	super.attachBaseContext(base);
	try {
		HookHelper.hookInstrumentation(base);
	} catch (Exception e) {
		e.printStackTrace();
	}
}

加载插件APK

因为我们要加载的Activity是在一个独立的APK文件中,所以这里要用到动态加载。实现Activity的动态加载需要对 Java类的加载机制 有一定程度的了解。

Android为我们提供了两个用于动态加载的类:DexClassLoader和PathClassLoader

  • DexClassLoader是一个可以从包含classes.dex实体的.jar或.apk文件中加载classes的类加载器。这个类加载器必须要一个app的私有、可写目录来缓存经过优化的classes(odex文件)
  • PathClassLoader是一个可以操作在本地文件系统的文件列表或目录中classes放入类加载器,但不可以从网络中加载classes。

动态加载APK需要使用DexClassLoader。首先,把下载下来的插件APK从Download目录复制到APP自己的私有存储空间:

private File copyToCache(String name) {
	String path = Environment.getExternalStorageDirectory().getPath() +
			File.separator + "Download" + File.separator + name;
	File source = new File(path);
	if (!source.exists() || !source.isFile()) {
		return null;
	}
	File dir = getCacheDir();
	if (!dir.exists() && dir.mkdirs()) {
		Log.i("MainActivity", "创建新文件夹");
	}
	File plugin = new File(dir.getPath() + File.separator + name);
	FileChannel input = null;
	FileChannel output = null;
	try {
		if (!plugin.exists() || plugin.createNewFile()) {
			Log.i("MainActivity", "创建新文件");
		}
		if (plugin.length() != source.length()) {
			input = new FileInputStream(source).getChannel();
			output = new FileOutputStream(plugin).getChannel();
			output.transferFrom(input, 0, input.size());
		}
	} catch (IOException e) {
		e.printStackTrace();
	} finally {
		if (output != null) {
			try {
				output.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		if (input != null) {
			try {
				input.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}
	return plugin;
}

然后,使用DexClassLoader加载:(注:下面代码我们创建的DexClassLoader对象实例要保留起来以备后用)

private void loadFile(final File file) {
	// 根据apk路径加载apk代码到DexClassLoader中
	DexClassLoader classLoader = new DexClassLoader(file.getPath(), file.getAbsolutePath(), null, ClassLoader.getSystemClassLoader());
	try {
		Class clazz = classLoader.loadClass("usage.ywb.wrapper.audio.ui.activity.MainActivity");
		startActivity(new Intent(MainActivity.this, clazz));
	} catch (Exception e) {
		e.printStackTrace();
	}
}

这两个过程涉及到I/O操作,实际操作中应该需要在一个单独的工作线程中执行。

运行一遍,发现在newActivity方法中出现一个运行时异常ClassNotFoundException
在这里插入图片描述
在这里插入图片描述
通过断点调试发现这里系统使用的类加载器是PathClassLoader,我们要加载的是一个APK文件,所以要把类加载器换成DexClassLoader(即上文中loadFile方法中使用的classLoader实例)。

经过修改,最终的InstrumentationProxy类(静态代理模式)如下:

public class InstrumentationProxy extends Instrumentation {

    private Instrumentation mInstrumentation;

    public InstrumentationProxy(Instrumentation mInstrumentation) {
        this.mInstrumentation = mInstrumentation;
    }

    public ActivityResult execStartActivity(
            Context who, IBinder contextThread, IBinder token, Activity target,
            Intent intent, int requestCode, Bundle options) {
        List<ResolveInfo> infoList = who.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_ALL);
        Intent mIntent;
        if (infoList.size() == 0) {
            mIntent = new Intent(who, ProxyActivity.class);
            mIntent.putExtra(HookHelper.TARGET_INTENT, intent);
        } else {
            mIntent = intent;
        }
        try {
           	// 在完成了代理Activity的替换工作后,我们仍然需要系统按照正常流程走下去。
        	// 所以,我们需要手动调用Instrumentation的execStartActivity方法。
            @SuppressLint("DiscouragedPrivateApi")
            Method execMethod = mInstrumentation.getClass().getDeclaredMethod("execStartActivity",
                    Context.class, IBinder.class, IBinder.class, Activity.class, Intent.class, int.class, Bundle.class);
            Object object = execMethod.invoke(mInstrumentation, who, contextThread, token,
                    target, mIntent, requestCode, options);
            if (object != null) {
                return (ActivityResult) object;
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    @Override
    public Activity newActivity(ClassLoader cl, String className, Intent intent) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
        if (intent != null) {
            Intent targetIntent = intent.getParcelableExtra(HookHelper.TARGET_INTENT);
            if (targetIntent != null && targetIntent.getComponent() != null) {
                ComponentName componentName = targetIntent.getComponent();
                ClassLoader classLoader = PluginClassLoaderHelper.getHelper().getClassLoader("audio.apk");
                return mInstrumentation.newActivity(classLoader, componentName.getClassName(), targetIntent);
            }
        }
        return mInstrumentation.newActivity(cl, className, intent);
    }
}

做完以上工作,已经能实现动态加载未安装APK中的Activity了。因为我们只对Intent做了一个“偷梁换柱”,Activity的完整创建过程还是由系统完成的,所以是保有生命周期的。

加载资源文件

虽然我们的Activity类已经替换成了插件中的类,但是系统在加载资源文件时使用的Resource还是主工程的,所以无法找到来自插件APK中的资源。

解决这个问题有两种方式:

  • 1,通过反射AssetManager#addAssetPath将插件中的资源合并到主工程,使得主工程能同时访问App和插件Apk的资源文件。
  • 2,保持插件的独立性,创建插件自己的AssetManager,加载插件中的Resource

这里我们以第二种方式为例

private Resources loadResources(String path) {
	//插件资源独立,该resource只能访问插件自己的资源
	Resources hostResources = HookManager.getInstance().getBaseContext().getResources();
	AssetManager assetManager = null;
	try {
		assetManager = AssetManager.class.newInstance();
		Method addAssetPathMethod = AssetManager.class.getMethod("addAssetPath", String.class);
		addAssetPathMethod.setAccessible(true);
		addAssetPathMethod.invoke(assetManager, path);
	} catch (Exception e) {
		e.printStackTrace();
	}
	return new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration());
}

InstrumentationProxy类中重写Instrumentation#callActivityOnCreate方法,在这里替换Resource

@Override
public void callActivityOnCreate(Activity activity, Bundle icicle) {
	injectActivity(activity);
	mInstrumentation.callActivityOnCreate(activity, icicle);
}

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public void callActivityOnCreate(Activity activity, Bundle icicle, PersistableBundle persistentState) {
	injectActivity(activity);
	mInstrumentation.callActivityOnCreate(activity, icicle, persistentState);
}

private void injectActivity(Activity activity) {
	Context base = activity.getBaseContext();
	try {
		Class<?> contextImplClass = Class.forName("android.app.ContextImpl");
		if (isIntentFromPlugin) {
			ReflectUtil.setField(contextImplClass, base, "mResources", HookManager.getInstance().getResource());
			Context context = createPluginContext(base);
			ReflectUtil.setField(ContextWrapper.class, activity, "mBase", context);
			ReflectUtil.setField(Activity.class, activity, "mApplication", mInstrumentation.newApplication(HookManager.getInstance().getClassLoader(),
					pluginApplicationClaaName, context));

			Intent targetIntent = activity.getIntent().getParcelableExtra(HookManager.TARGET_INTENT);
			ComponentName component = targetIntent.getComponent();
			Intent wrapperIntent = new Intent(activity.getIntent());
			wrapperIntent.setClassName(component.getPackageName(), component.getClassName());
			activity.setIntent(wrapperIntent);
		}
	} catch (Exception e) {
		e.printStackTrace();
	}
}

[参考文档]

  1. Android插件化主流框架和实现原理
  2. Android资源管理器过程分析 & 插件化实现Hook 资源管理器的实现
  3. 谈谈 Android 中的 PathClassLoader 和 DexClassLoader
  4. 深入理解Android插件化技术
  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值