【Android】Activity启动流程和插件化原理

Activity启动流程

Activity启动流程分析有很多文章了,为什么我要再写一篇,因为我觉得大部分的文章讲的都有点复杂,个人经验认为学习一种技术尽量从全局去看,否则会陷入细节而让自己对这种技术没有一个全局的概念,看了网上几篇文章,我总结归纳了一张图,图里我也加了很多注释,先看图我再补充一下细节

Activity启动流程.jpg

请求阶段:

ActivityManagerProxy是ActivityManagerService在app进程中的Binder代理对象。调用ActivityManagerProxy.startActivity()最后会调用ActivityManagerService.startActivity()。这样请求就到了ActivityManagerService

响应阶段:

在不考虑多进程的情况下,Activity的启动过程是一个Binder双向通信的过程。AMS要主动与app进程通信要依靠请求启动Activity阶段传过来的IBinder对象,这个IBinder对象就是上面介绍过的Instrumentation.execStartActivity()中的 whoThread对象,它实际上是一个ApplicationThreadProxy对象,用来和ApplicationThread通信。AMS通知app进程启动Activity是通过调用ApplicationThreadProxy.scheduleLaunchActivity()完成的。根据Binder通信,ApplicationThread.scheduleLaunchActivity()会被调用。

  1. scheduleLaunchActivity()将从AMS中传过来的参数封装成ActivityClientRecord对象,然后将消息发送给mH,mH是一个Handler对象。
  2. H是ActivityThread的内部类,继承自Handler,它在收到LAUNCH_ACTIVITY的消息后,会调用ActivityThread.handlerLaunchActivity()。
  3. handleLaunchActivity()主要调用了两个方法:performLaunchActivity()和handleResumeActivity()。performLaunchActivity()会完成Activity的创建,以及调用Activity的onCreate()、onStart()等方法。handleResumeActivity()会完成Activity.onResume()的调用。

插件化的实现

插件化的实现有很多种方式,我下面说的方式是我认为比较简单的方式,滴滴的插件化框架貌似就是这么实现的.

假如在插件中有一个未在AndroidManifest.xml注册的TargetActivity,我们想启动它,可以分为三步。

  1. 在AndroidManifest.xml中预先注册一个我们项目中没有的Activity,例如ProxyActivity。我们把这种行为称为插桩。
  2. 在请求启动Activity阶段,我们把TargetActivity替换成AndroidManifest中预先注册的ProxyActivity。
  3. 在AMS响应阶段,Activity实例产生之前,我们再做一个完全相反的动作。即把响应信息中要启动的ProxyActivity替换回TargetActivity。

第一步十分简单,没什么好说的。要实现第二步和第三步就需要用到Activity启动流程的知识了。 在Activity启动流程中,Instrumentation无论在请求阶段还是响应阶段都扮演着重要的角色。在请求阶段Instrumentation.execStartActivity()会被调用,而在响应阶段Instrumentation.newActivity()会被调用。因此如果我们可以Hook Instrumentation,那么我们就可以在execStartActivity()和newActivity()分别完成第二步和第三步中的功能。

ActivityThread中的Instrumentation在什么时候被创建:

public static void main(String[] args) {
    //...
    ActivityThread thread = new ActivityThread();
    thread.attach(false);
    //...
}

private void attach(boolean system) {
    sCurrentActivityThread = this;
    final IActivityManager mgr = ActivityManagerNative.getDefault();
    //与AMS通信
    mgr.attachApplication(mAppThread);
}

public static ActivityThread currentActivityThread() {
    return sCurrentActivityThread;
} 
  1. 在ActivityThread的main()方法中,ActivityThread会被初始化并最终把对象保存在静态的sCurrentActivityThread中。在一个app进程中只有一个ActivityThread实例sCurrentActivityThread。sCurrentActivityThread可以通过ActivityThread.currentActivityThread()拿到。
  2. attach()中,mgr.attachApplication(mAppThread)这段代码又是一个Binder双向通信的过程,它主要为创建Application对象服务。整个通信过程和Activity启动过程类似,我就不再详细介绍了。在通信的最后,ActivtiyThread.handleBindApplication()被调用,而在方法内部,Instrumentation被初始化。

总结:一个App进程,只有一个ActivityThread对象,这个对象保存在sCurrentActivityThread中,可以通过ActivityThread.currentActivityThread()获取。ActivityThread的mInstrumentation会在Application创建之前初始化。

Activity中的Instrumentation在什么时候被设置:

  1. Activtiy中的Instrumentation是通过Activity.attach()传进来的。
  2. Activity.attach()在介绍Activity启动流程时提到过。它会在ActivityThread.performLaunchActivity()中被调用。
  3. 这样ActivtyThread把自己内部的Instrumentation传递到了Activity中。

最终目的:Hook Instrumentation:

通过以上分析,我们知道,要Hook app的Instrumentation,只需要替换掉ActivityThread的Instrumentation即可。但是,Android SDK没有为我们提供任何关于ActivityThread的api。在滴滴的VirtualAPK插件化框架里重新声明了这些Android SDK没有提供的Framework层的类。这些类只有方法的声明,这样我们就可以使用这些Android SDK没有提供的类或隐藏的方法了。需要注意的一点是,AndroidStub应该只参与编译过程,这很简单,用compileOnly依赖就可以了。

  1. 接下来,通过反射替换ActivitThread的Instrumentation:
protected void hookInst rumentation() {
try {
    ActivityThread activityThread = ActivityThread.currentActivityThread();
    Instrumentation baseInstrumentation = activityThread.getInstrumentation();
    final VAInstrumentation instrumentation = createInstrumentation(baseInstrumentation);
    Reflector.with(activityThread).field("mInstrumentation").set(instrumentation);
} catch (Exception e) {
    Log.w(TAG, e);
}
}
public class VAInstrumentation extends Instrumentation {
    private Inst rumentation mBase ;
    private PluginManager mPluginManager;
    public VAInstrumentation(PluginManager pluginManager, Instrumentation base) {
        this.mPluginManager = pluginManager;
        this.mBase = base;
    }
} 
  1. 上面的VAInstrumentation是对系统Instrumentation的代理类。在VAInstrumentation的内部我们可以加入任何我们想要的逻辑。在Instrumentation.execStartActivity()执行前将我们要启动的Activity替换成预注册的ProxyActivity。
public class VAInstrumentation extends Instrumentation implements Handler.Callback {
    @override
    public ActivityResult execStartActivity(
        Context who, 
        IBinder contextThread, 
        IBinder token, 
        String target, 
        Intent intent, 
        int requestCode, 
        Bundle options) (
    injectIntent(intent);
    return mBase.execStartActivity (who, contextThread, token, target, intent, requestCode, options);
}
private void injectIntent(Intent intent) {
    if(intent.getComponent() != null) {
        String targetPackageName = intent.getComponent().getPackageName();
        String targetClassName = intent.getComponent().getClassName();
        //如果启动插件中的Activity
        if (!targetPackageName.equals(mContext.getPackageName())) (
            //将Activity的原始信息存入Intent中
            intent.putExtra(Constants.KEY_IS_PLUGIN, true);
            intent.putExtra (Constants.KEY_TARGET_ PACKAGE, targetPackageName);
            intent.putExtra(Constants.KEY_TARGET_ACTIVITY, targetClassName);
            //用ProxyActivity替换
            dispatchStubActivity(intent);
        }
    }
}
private void dispatchStubActivity(Intent intent) {
    String stubActivity = "com.like.virtualapk.ProxyActivity";
    intent.setClassName(mContext, stubActivity);
} 
  1. 在Instrumentation.newActivity()执行前将预注册的ProxyActivity替换回我们要启动的Activity。
@override
public Activity newActivity(ClassLoader cl, String className, Intent intent) {
    try {
        cl.loadClass(className);
    } catch (ClassNotFoundException e) {
        ComponentName component = getComponent(intent);
        if ( component == null) {
            return mBase.newActivity(cl, className, intent) ;
        }
        String targetClassName = component.getClassName();
        Log. i(TAG, String. format("newActivity[%s : &s/%s]", className, component.getPackageName(), targetClassName));
        Activity activity = mBase.newActivity(cl, targetClassName, intent);
        activity.setIntent(intent);
        return activity;
    }
    return mBase.newActivity(cl, className, intent);
}
public static boolean isIntentFromPlugin(Intent intent) {
    if ( intent = null) {
        return false;
    }
    return intent.getBooleanExtra(Constants.KEY_IS_PLUGIN, false);
}
public static ComponentName getComponent(Intent intent) {
    if (intent = null) {
        return null;
    }
    if(isIntentFromPlugin(intent)) {
        return new ComponentNameintent.getStringExtra(Constants.KEY_TARGET_PACKAGE),
            intent.getStringExtra(Constants.KEY_TARGET_ACTIVITY));
    }
    return intent.getComponent() ;
} 

加载插件资源:

  1. 反射调用AssetsManager的addAssetPath方法,将外部的apk路径添加进去,构建新的Resource对象。
  2. 通过DexClassLoader加载R.class,通过资源名称获取对应的id,通过上述构建的Resource和资源id获取资源对象。
/**
* 反射添加资源路径,并创建新的Resources 对象
*/
private Resources getPluginResources() {
    try {
        AssetManager assetManager = AssetManager.class.newInstance();
        //反射获取AssetManager的addAssetPath方法
        Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
        //将插件包地址添加进行
        addAssetPath.invoke(assetManager, apkDir+ File.separator+apkName);
        Resources superRes = context.getResources();
        //创建Resources
        Resources mResources = new Resources(assetManager, superRes.getDisplayMetrics(),
                superRes.getConfiguration());
        return mResources;
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}

/**
* 1. 先获取资源的名称对应的id(通过反射R.class文件的变量)
* 2. 再根据我们构造的Resources 获取对应的资源对象。
*/
public Drawable getApkDrawable(String drawableName){
    try {
        DexClassLoader dexClassLoader = new DexClassLoader(apkDir+File.separator+apkName,
        optimizedDirectoryFile.getPath(), null, context.getClassLoader());
 
        //通过使用apk自己的类加载器,反射出R类中相应的内部类进而获取我们需要的资源id
        Class<?> clazz = dexClassLoader.loadClass(apkPackageName + ".R$drawable");
        Field field = clazz.getDeclaredField(drawableName);
        int resId = field.getInt(R.id.class);//得到图片id
        Resources mResources = getPluginResources();
        assert mResources != null;
        return mResources.getDrawable(resId);
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    } catch (NoSuchFieldException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    }
    return null;
} 

常见的插件化框架:

  1. 静态代理 dynamic-load-apk最早使用ProxyActivity这种静态代理技术,由ProxyActivity去控制插件中PluginActivity的生命周期
  2. 动态替换(HOOK) 在实现原理上都是趋近于选择尽量少的hook,并通过在manifest中预埋一些组件实现对四大组件的动态插件化。像Replugin。
  3. 容器化框架 VirtualApp能够完全模拟app的运行环境,能够实现app的免安装运行和双开技术。
  4. Atlas是阿里的结合组件化和热修复技术的一个app基础框架,号称是一个容器化框架。

最后

一、面试合集

在这里插入图片描述

二、源码解析合集

在这里插入图片描述

三、开源框架合集

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

推荐阅读更多精彩内容