Android插件化启动Activity

支付宝APP本身更像是一个"空壳",里面可以搭载很多小功能,这些小功能都是以"插件"的形式存在,支持小功能的灵活配置,用户不想要某个功能,可以不显示出来。插件化开发是当下大型APP必备的一项技术。

插件化开发的核心难点

根据引子中所说,支付宝中各种各样的功能,都是插件形式存在的,那么具体是如何存在?
我们所说的插件,其实是apk文件,即xxx.apk
插件化开发的套路: 外壳app module + 多个插件plugin module + 插件框架层library module

  • 外壳app 负责整个app的外部架构,并且给插件提供入口组件(比如,用一个button作为"余额宝"的入口,点击button,进入"余额宝")
  • 多个插件plugin module,负责分开开发各个功能。严格来说,每个功能必须可以单独运行,也必须支持集成到外壳app时运行
  • 插件框架层library module,所有插件化的核心代码,都集中到这里。并且这个library要同时被外壳app和插件module引用

插件化所需的技术理论基础

学习插件化开发,首先要了解

Activity是如何启动的

在Activity里,开启另一个Activity,使用startActivity即可,但是startActivity之后,系统做了什么?

开始追踪源码(源码追踪基于SDK 28 - 9.0):

public class MainActivity extends AppCompatActivity() {
    private void xxxx() {
        Intent i = new Intent(this, XXXActivity.class);
        startActivity(i);
    }
}

那么,startActivity到底做了什么,点进去看找到下面的代码:

@Override
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);
    }
}

继续,追踪这两个startActivityForResult,直接到下面的代码:

public void startActivityForResult(@RequiresPermission Intent intent, int requestCode,
        @Nullable Bundle options) {
    if (mParent == null) {
        options = transferSpringboardActivityOptions(options);
        Instrumentation.ActivityResult ar =
            mInstrumentation.execStartActivity(
                this, mMainThread.getApplicationThread(), mToken, this,
                intent, requestCode, options); //注意看这里,mInstrumentation.execStartActivity
        if (ar != null) {
            mMainThread.sendActivityResult(
                mToken, mEmbeddedID, requestCode, ar.getResultCode(),
                ar.getResultData());
        }
        if (requestCode >= 0) {
           ···
            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);
        }
    }
}

mInstrumentation.execStartActivity在这里被执行,然而另一个分支mParent不为空时,会执行mParent.startActivityFromChild

public void startActivityFromChild(@NonNull Activity child, @RequiresPermission Intent intent,
        int requestCode, @Nullable Bundle options) {
    options = transferSpringboardActivityOptions(options);
    Instrumentation.ActivityResult ar =
        mInstrumentation.execStartActivity(
            this, mMainThread.getApplicationThread(), mToken, child,
            intent, requestCode, options); //然而,这里还是执行了mInstrumentation.execStartActivity
    if (ar != null) {
        mMainThread.sendActivityResult(
            mToken, child.mEmbeddedID, requestCode,
            ar.getResultCode(), ar.getResultData());
    }
    cancelInputsAndStartExitTransition(options);
}

然而,这里还是执行了mInstrumentation.execStartActivity,综上所述,startActivity最终都会执行到mInstrumentation.execStartActivity,那么继续跟踪这个execStartActivity

public ActivityResult execStartActivity(
        Context who, IBinder contextThread, IBinder token, Activity target,
        Intent intent, int requestCode, Bundle options) {
        ...省略一大段...
    try {
        intent.migrateExtraStreamToClipData();
        intent.prepareToLeaveProcess(who);
        int result = ActivityManager.getService() //这个不就是大名鼎鼎的AMS么
            .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;
}

注意这里有一个ActivityManager.getService(),其实就是安卓里大名鼎鼎的AMS(ActivityManagerService),负责对 Android四大组件(Activity,Service,Broadcast,ContentProvider)的管理,包括启动,生命周期管理等

Activity里面startActivity的追踪就到这里。

PS:其实,Activity不只是可以在Activity里启动,还可以使用getApplicationContext().startActivity(),有兴趣的可以去追踪一下,最终结论还是一样,都会执行AMS的startActivity

结论:我们通常在自己的Activity里调用startActivity之后,最终会执行AMS的startActivity,从而让启动的那个Activity具有生命周期。那么如果只是new一个Activity实例,它会不会具有生命周期呢?显而易见了

apk包(其实是压缩包)里的各个文件各自的作用

在Android Studio里,运行app,或者gradle执行assemble命令可以生成apk文件,那么我们解压apk文件之后,它里面的各种内部文件,各自都起到了什么作用呢?

classes.dex

classes.dex文件,工程里面的Java源码编译打包而成,包含了这个apk的所有Java类,我们拿到这个dex文件,就有能力反射创建其中的类对象

res目录

所有的资源文件,外壳app通过资源包,可以拿到包里面的任意资源,当然前提是宿主要创建对应资源包的Resources对象

resources.arsc

res下所有资源的映射

META-INF

app签名的一些东西

AndroidManifest.xml

清单文件

核心难点的解决方案

外壳app,作为一个"宿主"。插件apk中的所有东西,无论是classes.dex里的类,还是res资源,都是"宿主"之外的东西,那么宿主要想使用自己身外的类和资源,需要解决3个问题:

取得插件中的Activity的Class

解决方案:使用DexClassLoader它是专门加载外部apk的类加载器

取得插件中的资源

解决方案:使用hook技术,创建只属于外部插件的Resouces资源管理器

代理Activity生命周期

反射创建了插件中的Activity对象,但是它是没有生命周期的,不能像使用宿主自身的Activity一样拥有完整的生命周期

解决方案:使用代理Activity作为真正插件Activity的"傀儡"

核心代码结构

app module

外壳app很简单,唯一要说明的就是插件apk,放置在src/main/assets目录,只是为了演示demo方便

MyApp.java,只做了一件事,PluginManager.getInstance().init(this);,对PluginManager进行初始化并且赋予上下文

MainActivity.java只做了两件事:

(1)将assets里面的apk文件,通过工具类AssetUtilcopyAssetToCache方法,拷贝到了app的缓存目录下,然后使用PluginManager去加载这个apk:

String path = AssetUtil.copyAssetToCache(MainActivity.this, "plugin_module-debug.apk");
PluginManager.getInstance().loadPluginApk(path);

(2)跳转到代理Activity,并且传入真正要跳的目标Activity的name.

// 先跳到代理Activity,由代理Activity展示真正的Activity内容
Intent intent = new Intent(MainActivity.this, ProxyActivity.class);
intent.putExtra(PluginApkConst.TAG_CLASS_NAME, 
        PluginManager.getInstance().getPackageInfo().activities[0].name);
startActivity(intent);

plugin module

插件module十分简单,它的作用,就是生成插件apk,对它进行编译打包,取得apk文件即可。

两个重点:

  • 插件中的所有Activity,必须都集成来自plugin_libPluginBaseActivity,只有继承了,才具有插件化特征,能够被外壳app执行startActivity成功跳转
  • 插件内部的Activity跳转,上下文,必须使用PluginBaseActivityproxy变量
/**
 * 插件的第一个Activity
 */
public class MainActivity extends PluginBaseActivity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(proxy, Main2Activity.class);
                startActivity(intent);
            }
        });
    }
}
/**
 * 插件的第二个Activity
 */
public class Main2Activity extends PluginBaseActivity {

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main2);
    }

}

plugin library module

前面两个module都很简单,那么核心技术在哪里?答案就是插件框架层代码,是插件化开发技术的核心。这个module要同时被外壳app和插件module引用。其中,3个技术要点:

PluginManager类

它是一个单例,负责读取插件apk的内容,并且创建出专属于插件的类加载器DexClassLoader,资源管理器Resources,以及包信息PackageInfo并用public get方法公开出去。

import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.content.res.Resources;

import java.io.File;
import java.lang.reflect.Method;

import dalvik.system.DexClassLoader;

/**
 * 插件apk的管理类
 * <p>
 * loadPluginApk(String path);
 * init(Context context);
 * getPackageInfo();
 * getDexClassLoader();
 * getResources();
 */
public class PluginManager {

    //*****应该是单例模式,因为一个宿主app只需要一个插件管理器对象即可*****
    private PluginManager() {
    }

    private volatile static PluginManager instance; //volatile 保证每一次取的instance对象都是最新的

    public static PluginManager getInstance() {
        if (instance == null) {
            synchronized (PluginManager.class) {
                if (instance == null) {
                    instance = new PluginManager();
                }
            }
        }
        return instance;
    }

    private Context mContext; //上下文

    private PackageInfo packageInfo; //包信息
    private DexClassLoader dexClassLoader; //类加载器
    private Resources resources; //资源包

    public void init(Context context) {
        mContext = context.getApplicationContext(); //要用application 因为这是单例,直接用Activity对象作为上下文会导致内存泄漏
    }

    /**
     * 从插件apk中读出我们所需要的信息
     *
     * @param apkPath
     */
    public void loadPluginApk(String apkPath) {
        //先拿到包信息
        packageInfo = mContext.getPackageManager().getPackageArchiveInfo(apkPath, PackageManager.GET_ACTIVITIES); //只拿Activity
        if (packageInfo == null)
            throw new RuntimeException("插件加载失败"); //如果apkPath是传的错的,那就拿不到包信息,下面的代码也就不用执行

        //类加载器,DexClassLoader专门负责外部dex的类
        File outFile = mContext.getDir("odex", Context.MODE_PRIVATE);
        dexClassLoader = new DexClassLoader(apkPath, outFile.getAbsolutePath(), null, mContext.getClassLoader());

        //创建AssetManager,然后创建Resources
        try {
            AssetManager assetManager = AssetManager.class.newInstance();
            Method method = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
            method.invoke(assetManager, apkPath);
            resources = new Resources(assetManager,
                    mContext.getResources().getDisplayMetrics(),
                    mContext.getResources().getConfiguration());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    //把这3个玩意公开出去
    public PackageInfo getPackageInfo() {
        return packageInfo;
    }

    public DexClassLoader getDexClassLoader() {
        return dexClassLoader;
    }

    public Resources getResources() {
        return resources;
    }

    /**
     * 既然无论是宿主启动插件的Activity,还是插件内部的跳转都要使用ProxyActivity作为代理
     * 何不写一个公共方法以供调用呢?
     *
     * @param context
     * @param realActivityClassName
     */
    public void gotoActivity(Context context, String realActivityClassName) {
        Intent intent = new Intent(context, ProxyActivity.class);
        intent.putExtra(PluginApkConst.TAG_CLASS_NAME, realActivityClassName);
        context.startActivity(intent);
    }
}
public class PluginApkConst {
    public final static String TAG_CLASS_NAME = "className";
    public static final String TAG_FROM = "from";
}

ProxyActivity类

它作为一个代理,一个傀儡,宿主能够通过它,来间接地管理真正插件Activity的生命周期。那它是如何间接管理真正Activity的生命周期?用类似下面的代码:

import android.app.Activity;
import android.content.res.Resources;
import android.os.Bundle;

/**
 * 代理Activity
 * 作用:接收来自宿主的跳转意图,并且拿到其中的参数
 * 这里只能继承Activity,而不是AppCompatActivity,否则会报“空指针”
 * 原因是,AppCompatActivity会调用上下文,你问为啥?不知道啊,问谷歌大佬
 */
public class ProxyActivity extends Activity {

    private String realActivityName; //既然我只是个代理,那么自然有真正的Activity

    private IPlugin iPlugin;

    // 由ProxyActivity代为管理真正Activity的生命周期
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        realActivityName = getIntent().getStringExtra(PluginApkConst.TAG_CLASS_NAME); //宿主,将真正的跳转意图,放在了这个参数className中,
        //拿到realActivityName,接下来的工作,自然就是展示出真正的Activity
        try { //原则,反射创建RealActivity对象,但是,去拿这个它的class,只能用dexClassLoader
            Class<?> realActivityClz = PluginManager.getInstance().getDexClassLoader().loadClass(realActivityName);
            Object obj = realActivityClz.newInstance();
            if (obj instanceof IPlugin) { //所有的插件Activity,都必须是IPlugin的实现类
                iPlugin = (IPlugin) obj;
                Bundle bd = new Bundle();
                bd.putInt(PluginApkConst.TAG_FROM, IPlugin.FROM_EXTERNAL);
                iPlugin.attach(this);
                iPlugin.onCreate(bd); //反射创建的插件Activity的生命周期函数不会被执行,那么,就由ProxyActivity代为执行
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Override
    protected void onStart() {
        iPlugin.onStart();
        super.onStart();
    }

    @Override
    protected void onResume() {
        iPlugin.onResume();
        super.onResume();
    }

    @Override
    protected void onRestart() {
        iPlugin.onRestart();
        super.onRestart();
    }

    @Override
    protected void onPause() {
        iPlugin.onPause();
        super.onPause();
    }

    @Override
    protected void onStop() {
        iPlugin.onStop();
        super.onStop();
    }

    @Override
    protected void onDestroy() {
        iPlugin.onDestroy();
        super.onDestroy();
    }

    //然后,下面2个方法必须重写,因为插件中使用的是外部的类和资源,所以必须用对应的DexClassLoader
    @Override
    public ClassLoader getClassLoader() {
        ClassLoader classLoader = PluginManager.getInstance().getDexClassLoader();
        return classLoader != null ? classLoader : super.getClassLoader();
    }

    @Override
    public Resources getResources() {
        Resources resources = PluginManager.getInstance().getResources();
        return resources != null ? resources : super.getResources();
    }
}

前面PluginManager返回了专属于插件的类加载器DexClassLoader,资源管理器Resources,那么这个ProxyActivity真正展示的是插件的Activity内容,就要使用插件自己的类加载器和资源管理器

public class ProxyActivity extends Activity {},文中ProxyActivity继承的是android.app.Activity,而不是android.support.v7.app.AppCompatActivity,这是因为,AppCompatActivity会检测上下文context,从而导致空指针。至于更深层的原因,有兴趣的大佬可以继续挖掘,没兴趣的话直接用android.app.Activity即可

IPlugin接口

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;

/**
 * 插件Activity的接口规范
 */
public interface IPlugin {

    int FROM_INTERNAL = 0; //插件单独测试时的内部跳转
    int FROM_EXTERNAL = 1; //宿主执行的跳转逻辑

    /**
     * 给插件Activity指定上下文
     *
     * @param activity
     */
    void attach(Activity activity);

    // 以下全都是Activity生命周期函数,
    // 插件Activity本身,在被用作"插件"的时候不具备生命周期,由宿主里面的代理Activity类代为管理
    void onCreate(Bundle saveInstanceState);

    void onStart();

    void onResume();

    void onRestart();

    void onPause();

    void onStop();

    void onDestroy();

    void onActivityResult(int requestCode, int resultCode, Intent data);
}

PluginBaseActivity抽象类

插件module中也许不只一个Activity,我们启动插件Activity之后,插件内部如果需要跳转,仍然要遵守插件化的规则,那就给他们创建一个共同的父类PluginBaseActivity

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;

import androidx.appcompat.app.AppCompatActivity;

/**
 * 插件Activity的基类,插件中的所有Activity,都要继承它
 */
public abstract class PluginBaseActivity extends AppCompatActivity implements IPlugin {

    private final String TAG = "PluginBaseActivityTag";
    protected Activity proxy; //上下文

    //这里基本上都在重写原本Activity的函数,因为 要兼容"插件单独测试" 和 "集成到宿主整体测试",所以要进行情况区分
    private int from = IPlugin.FROM_INTERNAL; //默认是"插件单独测试"

    @Override
    public void attach(Activity proxyActivity) {
        proxy = proxyActivity;
    }

    @Override
    public void onCreate(Bundle saveInstanceState) {
        if (saveInstanceState != null)
            from = saveInstanceState.getInt(PluginApkConst.TAG_FROM);

        if (from == IPlugin.FROM_INTERNAL) {
            super.onCreate(saveInstanceState);
            proxy = this; //如果是从内部跳转,那就将上下文定为自己
        }
    }

    @Override
    public void onStart() {
        if (from == IPlugin.FROM_INTERNAL) {
            super.onStart();
        } else {
            Log.d(TAG, "宿主启动:onStart()");
        }
    }

    @Override
    public void onResume() {
        if (from == IPlugin.FROM_INTERNAL) {
            super.onResume();
        } else {
            Log.d(TAG, "宿主启动:onResume()");
        }
    }

    @Override
    public void onRestart() {
        if (from == IPlugin.FROM_INTERNAL) {
            super.onRestart();
        } else {
            Log.d(TAG, "宿主启动:onRestart()");
        }
    }

    @Override
    public void onPause() {
        if (from == IPlugin.FROM_INTERNAL) {
            super.onPause();
        } else {
            Log.d(TAG, "宿主启动:onPause()");
        }
    }

    @Override
    public void onStop() {
        if (from == IPlugin.FROM_INTERNAL) {
            super.onStop();
        } else {
            Log.d(TAG, "宿主启动:onStop()");
        }
    }

    @Override
    public void onDestroy() {
        if (from == IPlugin.FROM_INTERNAL) {
            super.onDestroy();
        } else {
            Log.d(TAG, "宿主启动:onDestroy()");
        }
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (from == IPlugin.FROM_INTERNAL) {
            super.onActivityResult(requestCode, resultCode, data);
        } else {
            Log.d(TAG, "宿主启动:onActivityResult()");
        }
    }

    //下面是几个生命周期之外的重写函数
    @Override
    public void setContentView(int layoutResID) { //设置contentView分情况
        if (from == IPlugin.FROM_INTERNAL) {
            super.setContentView(layoutResID);
        } else {
            proxy.setContentView(layoutResID);
        }
    }

    @Override
    public View findViewById(int id) {
        if (from == FROM_INTERNAL) {
            return super.findViewById(id);
        } else {
            return proxy.findViewById(id);
        }
    }

    @Override
    public void startActivity(Intent intent) { //同理
        if (from == IPlugin.FROM_INTERNAL) {
            super.startActivity(intent); //原intent只能用于插件单独运行时
        } else {
            //如果是集成模式下,插件内的跳转,控制权仍然是在宿主上下文里面,所以--!
            //先跳到代理Activity,由代理Activity展示真正的Activity内容
            PluginManager.getInstance().gotoActivity(proxy, intent.getComponent().getClassName());
        }
    }
}

PluginBaseActivity抽象类中,3个重点需要特别说明

  1. 插件module需要单独测试,也需要作为插件来集成测试,所以这里IPlugin接口中定义了FROM_INTERNALFROM_EXTERNAL进行情形区分

  2. 除了IPlugin必须实现的一些生命周期方法之外,最后还新增了3个方法:setContentViewfindViewByIdstartActivity,设置布局,寻找组件,跳转Activity,也是需要区分单测还是集成测试的,所以,也要做if/else判定

  3. 上面说的startActivity,当从外部跳转,也就是宿主来启动插件Activity的时候,也只能跳到ProxyActivity,然后把真正的目标Activity放在参数中

  4. plugin library module下的AndroidManifest.xml注册

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="cn.appblog.plugin_lib" >
    <application>
        <activity android:name=".ProxyActivity" />
    </application>
</manifest>

如何使用Demo

更改plugin_module的内容,重新生成一个apk,放到app/src/main/assets目录,文件名必须和外壳app内写的一样,运行外壳app即可。

最终效果展示

集成测试,由外壳app启动插件Activity

集成测试

插件单独测试

版权声明:
作者:Joe.Ye
链接:https://www.appblog.cn/index.php/2023/03/29/android-plugin-launching-activity/
来源:APP全栈技术分享
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
打赏
海报
Android插件化启动Activity
支付宝APP本身更像是一个"空壳",里面可以搭载很多小功能,这些小功能都是以"插件"的形式存在,支持小功能的灵活配置,用户不想要某个功……
<<上一篇
下一篇>>
文章目录
关闭
目 录