学无止境,学以致用。
在实际开发中,需求是随着业务的发展不断变化的。以登录来说,看似简单,但随着业务性质的不同,其场景需求就会不断发生变化。
比如微信、QQ这种,他们是强登录的类型,就是在应用开启的第一步就是必须先进行登录的动作,否则无法使用后续的功能。但如淘宝、京东这类软件,用户只是先浏览商品,在加入购物车或者下订单、购买等环节才需要登录,如果这种类型的软件也做成一进来先必须要登录的话明显是不合理的。但问题也随之而来,以加入购物车为例,用户浏览完商品,点击加入购物车,发现用户未登录,那么跳转到登录页面进行登录,登录完之后还需要为用户执行之前的意图,也就是加入购物车或者跳转到购物车页面的操作。纳尼,有点小复杂,但这好像也难不倒我们伟大的程序猿(工程师)们,在需要用户信息的地方加个判断,没登录就登录,将参数和后续意图传过去,登录完了在根据意图跳转到对应的页面不就可以了么?so easy,打完下班。
等等,对于一个商城而言,大部分都会以插件化或者组件化的形式进行开发,而且有可能你负责的业务与登录模块其实是做了分离的,也就意味着负责登录模块的同事压根都不会知道你会传什么参数给他,他也不会直接参与处理具体业务的逻辑。嗯,一脸懵逼了吧。这时候我们需要从根上去想办法解决这个问题,比如我们所有的页面启动都会调用的方法,然后从此点入手,来改变逻辑走向为我们需要的走向。
好吧,说了那么一大堆,来点实际的。今天我们就Hook AMS来实现我们需要的功能,这个还是需要一点关于PMS和AMS相关的知识点,要是您不太熟悉的话,建议可以先看一看前面关于PMS与AMS的流程。
https://blog.csdn.net/calm1516/article/details/114170934 https://blog.csdn.net/calm1516/article/details/106947647
我们都知道启动Activity的函数是startActivity,那么我们就从这里入手。寻找其中可供我们Hook的点。
//Activity.java
public void startActivity(Intent intent) {
this.startActivity(intent, null);
}
//Activity.java
public void startActivity(Intent intent, @Nullable Bundle options) {
if (options != null) {
startActivityForResult(intent, -1, options);
} else {
// Note we want to go through this call for compatibility with
// applications that may have overridden the method.
startActivityForResult(intent, -1);
}
}
//Activity.java
public void startActivityForResult(@RequiresPermission Intent intent, int requestCode) {
startActivityForResult(intent, requestCode, null);
}
//Activity.java
public void startActivityForResult(@RequiresPermission Intent intent, int requestCode,
@Nullable Bundle options) {
if (mParent == null) {
options = transferSpringboardActivityOptions(options);
//不管mParent是否为null,其实最终都是会调用下面这样的一段代码
Instrumentation.ActivityResult ar =
mInstrumentation.execStartActivity(
this, mMainThread.getApplicationThread(), mToken, this,
intent, requestCode, options);
if (ar != null) {
mMainThread.sendActivityResult(
mToken, mEmbeddedID, requestCode, ar.getResultCode(),
ar.getResultData());
}
if (requestCode >= 0) {
// If this start is requesting a result, we can avoid making
// the activity visible until the result is received. Setting
// this code during onCreate(Bundle savedInstanceState) or onResume() will keep the
// activity hidden during this time, to avoid flickering.
// This can only be done when a result is requested because
// that guarantees we will get information back when the
// activity is finished, no matter what happens to it.
mStartedActivity = true;
}
cancelInputsAndStartExitTransition(options);
// TODO Consider clearing/flushing other event sources and events for child windows.
} else {
if (options != null) {
mParent.startActivityFromChild(this, intent, requestCode, options);
} else {
// Note we want to go through this method for compatibility with
// existing applications that may have overridden it.
mParent.startActivityFromChild(this, intent, requestCode);
}
}
}
//Activity.java
public void startActivityFromChild(@NonNull Activity child, @RequiresPermission Intent intent,
int requestCode, @Nullable Bundle options) {
options = transferSpringboardActivityOptions(options);
//mParent不会null的时候,其实调用的最终跟上面是一样的代码
Instrumentation.ActivityResult ar =
mInstrumentation.execStartActivity(
this, mMainThread.getApplicationThread(), mToken, child,
intent, requestCode, options);
if (ar != null) {
mMainThread.sendActivityResult(
mToken, child.mEmbeddedID, requestCode,
ar.getResultCode(), ar.getResultData());
}
cancelInputsAndStartExitTransition(options);
}
//这个非常重要,其实所有的版本在执行到这都基本没太大区别,区别在于execStartActivity这个方法,重头戏来了
Instrumentation.ActivityResult ar =
mInstrumentation.execStartActivity(
this, mMainThread.getApplicationThread(), mToken, child,
intent, requestCode, options);
//Instrumentation.java
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
IApplicationThread whoThread = (IApplicationThread) contextThread;
Uri referrer = target != null ? target.onProvideReferrer() : null;
if (referrer != null) {
intent.putExtra(Intent.EXTRA_REFERRER, referrer);
}
if (mActivityMonitors != null) {
synchronized (mSync) {
final int N = mActivityMonitors.size();
for (int i=0; i<N; i++) {
final ActivityMonitor am = mActivityMonitors.get(i);
ActivityResult result = null;
if (am.ignoreMatchingSpecificIntents()) {
result = am.onStartActivity(intent);
}
if (result != null) {
am.mHits++;
return result;
} else if (am.match(who, null, intent)) {
am.mHits++;
if (am.isBlocking()) {
return requestCode >= 0 ? am.getResult() : null;
}
break;
}
}
}
}
try {
intent.migrateExtraStreamToClipData();
intent.prepareToLeaveProcess(who);
//这里执行真正的启动
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);
} catch (RemoteException e) {
throw new RuntimeException("Failure from system", e);
}
return null;
}
ok,到这个函数的时候我们马上进入重点环节,因为里面的函数随着版本的不同发生了很多变化了。
我们在这里做下记录:
29版本:
int result = ActivityTaskManager.getService()
.startActivity(whoThread, who.getBasePackageName(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()),
token, target != null ? target.mEmbeddedID : null,
requestCode, 0, null, options);
//ActivityTaskManager.java
/** @hide */
public static IActivityTaskManager getService() {
return IActivityTaskManagerSingleton.get();
}
@UnsupportedAppUsage(trackingBug = 129726065)
private static final Singleton<IActivityTaskManager> IActivityTaskManagerSingleton =
new Singleton<IActivityTaskManager>() {
@Override
protected IActivityTaskManager create() {
final IBinder b = ServiceManager.getService(Context.ACTIVITY_TASK_SERVICE);
return IActivityTaskManager.Stub.asInterface(b);
}
};
//Singleton.java
//这个类是一个抽象类,其实就是负责创建T模板单例
public abstract class Singleton<T> {
@UnsupportedAppUsage
private T mInstance;
protected abstract T create();
@UnsupportedAppUsage
public final T get() {
synchronized (this) {
if (mInstance == null) {
mInstance = create();
}
return mInstance;
}
}
}
25版本
int result = ActivityManagerNative.getDefault()
.startActivity(whoThread, who.getBasePackageName(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()),
token, target != null ? target.mEmbeddedID : null,
requestCode, 0, null, options);
static public IActivityManager getDefault() {
return gDefault.get();
}
private static final Singleton<IActivityManager> gDefault = new Singleton<IActivityManager>() {
protected IActivityManager create() {
IBinder b = ServiceManager.getService("activity");
if (false) {
Log.v("ActivityManager", "default service binder = " + b);
}
IActivityManager am = asInterface(b);
if (false) {
Log.v("ActivityManager", "default service = " + am);
}
return am;
}
};
26版本
int result = ActivityManager.getService()
.startActivity(whoThread, who.getBasePackageName(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()),
token, target != null ? target.mEmbeddedID : null,
requestCode, 0, null, options);
public static IActivityManager getService() {
return IActivityManagerSingleton.get();
}
private static final Singleton<IActivityManager> IActivityManagerSingleton =
new Singleton<IActivityManager>() {
@Override
protected IActivityManager create() {
final IBinder b = ServiceManager.getService(Context.ACTIVITY_SERVICE);
final IActivityManager am = IActivityManager.Stub.asInterface(b);
return am;
}
};
好了,我们对比25、26、29版本发现获取AMS的方法是有不同的,对应的静态实例也有不同。25为gDefault,26为IActivityManagerSingleton,29为IActivityTaskManagerSingleton。这时候您一定会想到我们为什么要花那么大的力气来找这个静态的实例,嗯,因为静态的才能保证是系统所创建的那个独一份啊,我们只是要改变一些执行流程,所以对象嘛还是得系统创建的那个,不能我们自己创建哈,这个在Hook技术中十分重要。好吧,点咱们是找到了,接来下开始我们的Hook(为所欲为)吧。
//核心思想就是反射加动态代理 偷梁换柱
public class HookAms {
private Context mContext;
public void hookAms(Context context) throws Exception{
this.mContext = context.getApplicationContext();
Field field = null;
//我们前面找不同的版本以及版本静态变量的字段就是为了用在此处
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.O){
Class<?> clz = Class.forName("android.app.ActivityManagerNative");
field = clz.getDeclaredField("gDefault");
}else if(Build.VERSION.SDK_INT <= Build.VERSION_CODES.P){
Class<?> clz = Class.forName("android.app.ActivityManager");
field = clz.getDeclaredField("IActivityManagerSingleton");
}else {
Class<?> clz = Class.forName("android.app.ActivityTaskManager");
field = clz.getDeclaredField("IActivityTaskManagerSingleton");
}
field.setAccessible(true);
Object o = field.get(null);
Class<?> singleton = Class.forName("android.util.Singleton");
Field mInstance = singleton.getDeclaredField("mInstance");
mInstance.setAccessible(true);
//Android 10在Application中hook会失败,manager返回null
//hook的时机该如何把握
Object manager = mInstance.get(o);
Class<?> iAm = null;
if(Build.VERSION.SDK_INT <= Build.VERSION_CODES.P){
iAm = Class.forName("android.app.IActivityManager");
}else {
iAm = Class.forName("android.app.IActivityTaskManager");
}
Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
new Class[]{iAm},new AmsInvocationHandler(manager));
mInstance.set(o,proxy);
}
private class AmsInvocationHandler implements InvocationHandler{
private Object mManager;
public AmsInvocationHandler(Object o){
this.mManager = o;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Log.e("CALM",method.getName());
if(method.getName().equals("startActivity")){
Intent intent = null;
for (int i = 0; i < args.length; i++){
Object arg = args[i];
if(arg instanceof Intent){
intent = (Intent) arg;
break;
}
}
SharedPreferences sharedPreferences = mContext.getSharedPreferences("CALM",Context.MODE_PRIVATE);
//未登录
if(!sharedPreferences.getBoolean("login",false)){
//取得真实的意图,在登录完成后会接着执行该意图
String jumpActivity = intent.getComponent().getClassName();
if(!jumpActivity.equals("com.calm.logincenter.LoginActivity")
&& !jumpActivity.equals("com.calm.logincenter.TowActivity")){
//将这个class传过去
intent.putExtra("jumpClass",jumpActivity);
//替换class为登录页
ComponentName componentName = new ComponentName(mContext,LoginActivity.class);
intent.setComponent(componentName);
}
}
}
return method.invoke(mManager,args);
}
}
}
上面的代码就实现了拦截的操作,跳转到登录页面。我在这里将不需要登录的是写死了,实际中可采用文件配置的方式,这样在代码中完全不用做任何额外的操作,直接在配置文件配置就可以了。
完成后又如何接着执行我们的真实意图呢,其实蛮简单了。所有的参数其实都是放在intent里的,我们在这里只是改变了跳转的class,添加了jumpClass就是改变之前真实的class,我们也传过去了,其余的都没做任何变化。
//我们可以在登录页面取出真实的class以及相关参数
jumpClass = getIntent().getStringExtra("jumpClass");
mBundle = getIntent().getExtras();
//在登录成功的地方在将意图和参数重新放回去,就达到了不改变原有意图的目的了
Intent intent = new Intent();
ComponentName componentName = new ComponentName(this,jumpClass);
intent.setComponent(componentName);
if(mBundle != null){
intent.putExtras(mBundle);
}
startActivity(intent);
ok,主要的代码都贴完了,其实代码非常简单,主要是思路的理解。对于版本兼容的问题需要对需要兼容的版本都去找下那个静态成员变量是否正确,基本就可以实现了。还有就是测试的时候发现在29版本Hook的时机问题,要是在Application执行的话Hook会失败,放到有活动真正创建的时候就没问题了,要是有朋友知道什么原因的话欢迎留言指出。
国际惯例:代码地址