草庐IT

Android插件化动态加载apk

walkingCoder 2023-04-19 原文
 

什么是插件化动态加载apk?

支付宝是万能的,既可以淘票票看电影,又可以买车票,还可以开共享单车,这些都是支付宝的开发人员开发维护的么?显然不是,那么他是怎么做到的呢?是使用了动态加载apk的解决方案

怎么动态加载apk呢?

支付宝作为一个宿主apk提前将要集成的apk作为一个插件(plugin)下载到本地,然后当使用该plugin(apk)的时候再去加载对应plugin(apk)的资源文件以及对应的native页面。就是不去安装plugin(apk)就可以直接运行该plugin(apk)中的页面。

动态加载plugin(apk)分析

怎么调用一个apk中的页面呢?我们可以动态加载plugin中的文件资源使其以伪宿主身份运行在宿主apk中。以加载一个Activity页面来作为例子。

要让插件中的Activity运行起来,我们可以在宿主中创建一个Activity,然后去手动创建插件中的Acitivity的实例,然后使用宿主apk中Activity的生命周期去调用插件Activity的生命周期,这样就可以让Plugin中的Activity运行起来。

<li>Plugin中Activity生命周期的处理

我们可以在宿主中使用一个特殊的Activity,这个Activity是一个空壳,没有任何页面。但是它有实际的Activity的生命周期,这样我们可以通过这个Activity的生命周期去调用我们自己创建的Plugin中的Activity中的生命周期,实现了Plugin中的Activity的伪生命周期。这个宿主Activity命名为ProxyActivity。

<li>Plugin中资源文件的获取

使用AssetManager去得到Plugin包中的资源文件。

加载Plugin实现

第一步 PluginInterface

我们的宿主要提供一套标准,这套标准用来规范宿主与Plugin之间的上下文以及生命周期关系的标准。我们称之为:PluginInterface。这个标准涉及到Activity生命周期以及上下文,定义如下:

public interface PluginInterface {
    void onCreate(Bundle saveInstance);
    void attachContext(FragmentActivity context);
​
    void onStart();
​
    void onResume();
​
    void onRestart();
​
    void onDestroy();
​
    void onStop();
​
    void onPause();
}

我们新建一个android依赖库plugin,依赖库中只有一个PluginInterface接口,这个interface作为一个依赖库的形式存在于宿主与Plugin中。宿主gradle与plugin gradle都引用这个库。

compile project(':plugin')

为了使得编译起来更方便,我这里将宿主apk,插件plugin(项目中称之为otherapk)与依赖库plugin放在同一个项目下,只不过这个项目有两个module。

第二步 PluginManager

宿主需要一套工具,来管理加载PluginApk以及获取PluginApk中资源文件,就叫PluginManager。

获取PluginApk的字节码文件对象

DexClassLoader是一个类加载器,可以用来从.jar和.apk文件中加载class。可以用来加载执行没用和应用程序一起安装的那部分代码。

我们要拿到Plugin中的字节码文件对象,需要拿到Plugin对应的DexClassLoader可以使用DexClassLoaderDexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent)方法。

dexPath:被解压的apk路径,不能为空。

optimizedDirectory:解压后的.dex文件的存储路径,不能为空。这个路径强烈建议使用应用程序的私有路径,不要放到sdcard上,否则代码容易被注入攻击。

libraryPath:os库的存放路径,可以为空,若有os库,必须填写。

parent:父亲加载器,一般为context.getClassLoader(),使用当前上下文的类加载器。

获取PluginApk中的Resource

我们可以使用Resource提供的下面的构造:

public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
        this(assets, metrics, config, CompatibilityInfo.DEFAULT_COMPATIBILITY_INFO);
    }

由于要获取PluginApk中的资源,所以这个assets对象应当是PluginApk中的资源对象;而对于一款手机的DisplayMetrics和Configuration来说,无论是宿主还是PluginApk获取的值都是一样的,所以可以使用宿主的值。

获取AssetManager对象

public final int addAssetPath(String path) {
        synchronized (this) {
            int res = addAssetPathNative(path);
            makeStringBlocks(mStringBlocks);
            return res;
        }
    }

这个path也就是PluginApk包在手机中的位置,由于这个方法被hide 了,我们需要使用反射。

AssetManager assets = AssetManager.class.newInstance();
//方法名  参数
Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
addAssetPath.invoke(assets, dexPath);

到这里,成功拿到了PluginApk的DexClassLoader和Resources。

PluginManager完整代码:

public class PluginManager {
​
    public static PluginManager instacne;
    private Context context;
    private DexClassLoader pluginDexClassLoader;
    private Resources pluginResource;
    private PackageInfo pluginPackageArchiveInfo;
​
    private PluginManager() {
    }
​
    public static PluginManager getInstacne() {
        if (instacne == null) {
            synchronized (PluginManager.class) {
                if (instacne == null) {
                    instacne = new PluginManager();
                }
            }
        }
        return instacne;
    }
​
    public void setContext(Context context) {
        this.context = context.getApplicationContext();
    }
​
    public PackageInfo getPluginPackageArchiveInfo() {
        return pluginPackageArchiveInfo;
    }
​
    public void loadApk(String dexPath) {
        pluginDexClassLoader = new DexClassLoader(dexPath, context.getDir("dex", Context.MODE_PRIVATE).getAbsolutePath(), null, context.getClassLoader());
        //拿到别的apk包下的入口Activity
        pluginPackageArchiveInfo = context.getPackageManager().getPackageArchiveInfo(dexPath, PackageManager.GET_ACTIVITIES);
​
        AssetManager assets = null;
        try {
            assets = AssetManager.class.newInstance();
            Method addAssetPaht = AssetManager.class.getMethod("addAssetPath", String.class);
            addAssetPaht.invoke(assets,dexPath);
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        pluginResource = new Resources(assets, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());
​
    }
    
    public DexClassLoader getPluginDexClassLoader(){
        return pluginDexClassLoader;
    }
    
    public Resources getPluginResource(){
        return pluginResource;
    }
}

第三步 ProxyActivity 代理Activity

ProxyActivity是宿主的Activity,这个ProxyActivity只是一个空壳,提供一套生命周期和上下文给我们自己创建的PluginActivity的的实例用的。

我们自己加载的PluginActivity实例只是一个对象,没有任何意义的,要给它套上生命周期,给他的上下文赋值

具体实现思路

启动PluginActivity时,先去启动ProxyActivity,然后再ProxyAcitivity中的oCreate方法中去创建PluginActivity的实例,然后去调用PluginActivity的onCreate方法。在ProxyActivity的onResume方法中调用PluginActivity的onResume方法等等。

记得重写ProxyActivity的getResources,因为这个时候要拿到的getResources是Plugin的

public class ProxyActivity extends AppCompatActivity {
​
    private PluginInterface pluginInterface;
​
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_proxy);
​
        //拿到要启动的Activity
        String className=getIntent().getStringExtra("className");
​
​
        try {
            //加载该Acitivity的字节码对象
            Class<?> aClass = PluginManager.getInstacne().getPluginDexClassLoader().loadClass(className);
            //创建该Activity的实例
            Object newInstance = aClass.newInstance();
            //程序健壮性检查
            if (newInstance instanceof  PluginInterface){
                pluginInterface= (PluginInterface) newInstance;
                //将代理Activity的实例传递给三方Activity
                pluginInterface.attachContext(this);
                //创建bundle用来与三方apk传输数据
                Bundle bundle=new Bundle();
                //调用三方Activity的onCreate
                pluginInterface.onCreate(bundle);
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
​
    /**
     * 很关键
     * 三方调用拿到对应加载的三方Resource
     * @return
     */
    public Resources getResource(){
        return PluginManager.getInstacne().getPluginResource();
    }
​
    public void startActivity(Intent intent){
        Intent newIntent=new Intent(this,ProxyActivity.class);
        newIntent.putExtra("className",intent.getComponent().getClassName());
        super.startActivity(newIntent);
    }
​
    @Override
    protected void onStart() {
        pluginInterface.onStart();
        super.onStart();
    }
​
    @Override
    protected void onResume() {
        pluginInterface.onResume();
        super.onResume();
    }
​
    @Override
    protected void onPause() {
        pluginInterface.onPause();
        super.onPause();
    }
​
    @Override
    protected void onStop() {
        pluginInterface.onStop();
        super.onStop();
    }
​
    @Override
    protected void onDestroy() {
        pluginInterface.onDestory();
        super.onDestroy();
    }
​
    @Override
    protected void onRestart() {
        pluginInterface.onRestart();
        super.onRestart();
    }
}

第四步 PluginApk的BaseActivity的构建

public class BaseActivity extends AppCompatActivity implements PluginInterface{
​
    //这里命名为protected 以便于子类使用
    protected AppCompatActivity thisContex;
​
    @Override
    public void onCreate(Bundle bundle) {
​
    }
​
    @Override
    public void setContentView(@LayoutRes int layoutResID) {
        thisContex.setContentView(layoutResID);
    }
​
    @Override
    public void setContentView(View view) {
        thisContex.setContentView(view);
    }
​
    @Override
    public void setContentView(View view, ViewGroup.LayoutParams params) {
        thisContex.setContentView(view, params);
    }
​
    @NonNull
    @Override
    public LayoutInflater getLayoutInflater() {
        return thisContex.getLayoutInflater();
    }
​
    @Override
    public Window getWindow() {
        return thisContex.getWindow();
    }
​
    @Override
    public View findViewById(@IdRes int id) {
        return thisContex.findViewById(id);
    }
​
    @Override
    public void attachContext(AppCompatActivity context) {
        thisContex=context;
    }
​
    @Override
    public ClassLoader getClassLoader() {
        return thisContex.getClassLoader();
    }
​
    @Override
    public WindowManager getWindowManager() {
        return thisContex.getWindowManager();
    }
​
    @Override
    public ApplicationInfo getApplicationInfo() {
        return thisContex.getApplicationInfo();
    }
​
    @Override
    public void finish() {
        thisContex.finish();
    }
​
    @Override
    public void onStart() {
​
    }
​
    @Override
    public void onResume() {
​
    }
​
    @Override
    public void onPause() {
​
    }
​
    @Override
    public void onStop() {
​
    }
​
    @Override
    public void onDestory() {
​
    }
​
    @Override
    public void onRestart() {
​
    }
​
    @Override
    protected void onSaveInstanceState(Bundle outState) {
        
    }
​
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return false;
    }
​
    @Override
    public void onBackPressed() {
        thisContex.onBackPressed();
    }
​
    @Override
    public void startActivity(Intent intent) {
        thisContex.startActivity(intent);
    }
}

PluginMainActivity

public class PluginMainActivity extends BaseActivity implements View.OnClickListener {
​
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_plugin_main);
​
        findViewById(R.id.btn).setOnClickListener(this);
    }
​
    @Override
    public void onClick(View view) {
        startActivity(new Intent(thisContext,SecondActivity.class));
    }
}
在宿主中启动PluginMainActivity

public class MainActivity extends AppCompatActivity {
​
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
​
    public void loadPlugin(View view){
          ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE,Manifest.permission.READ_EXTERNAL_STORAGE},100);
    }
​
    public void startPlugin(View view){
        Intent intent=new Intent(this,ProxyActivity.class);
        String otherapkName=PluginManager.getInstance().getPluginPackageAricheInfo().activities[0].name;
        intent.putExtra("className",otherapkName);
        startActivity(intent);
    }
​
​
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        PluginManager.getInstance().setContext(this);
        PluginManager.getInstance().loadApk(Environment.getExternalStorageDirectory().getAbsolutePath()+"/pluginapk-debug.apk");
    }
}

最后

加权限

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

把pluginapk-debug.apk放在sd卡中,实际是下载完放在sd 某个位置,先加载插件,后运行。搞定。就能打开插件中的secondActivity。

github地址:GitHub - walkingCoder/PluginDemo: 插件化apk,小试

有关Android插件化动态加载apk的更多相关文章

  1. ruby - 如何每月在 Heroku 运行一次 Scheduler 插件? - 2

    在选择我想要运行操作的频率时,唯一的选项是“每天”、“每小时”和“每10分钟”。谢谢!我想为我的Rails3.1应用程序运行调度程序。 最佳答案 这不是一个优雅的解决方案,但您可以安排它每天运行,并在实际开始工作之前检查日期是否为当月的第一天。 关于ruby-如何每月在Heroku运行一次Scheduler插件?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/8692687/

  2. ruby-on-rails - 无法使用 Rails 3.2 创建插件? - 2

    我对最新版本的Rails有疑问。我创建了一个新应用程序(railsnewMyProject),但我没有脚本/生成,只有脚本/rails,当我输入ruby./script/railsgeneratepluginmy_plugin"Couldnotfindgeneratorplugin.".你知道如何生成插件模板吗?没有这个命令可以创建插件吗?PS:我正在使用Rails3.2.1和ruby​​1.8.7[universal-darwin11.0] 最佳答案 随着Rails3.2.0的发布,插件生成器已经被移除。查看变更日志here.现在

  3. ruby - 如何在续集中重新加载表模式? - 2

    鉴于我有以下迁移:Sequel.migrationdoupdoalter_table:usersdoadd_column:is_admin,:default=>falseend#SequelrunsaDESCRIBEtablestatement,whenthemodelisloaded.#Atthispoint,itdoesnotknowthatusershaveais_adminflag.#Soitfails.@user=User.find(:email=>"admin@fancy-startup.example")@user.is_admin=true@user.save!ende

  4. ruby - RuntimeError(自动加载常量 Apps 多线程时检测到循环依赖 - 2

    我收到这个错误:RuntimeError(自动加载常量Apps时检测到循环依赖当我使用多线程时。下面是我的代码。为什么会这样?我尝试多线程的原因是因为我正在编写一个HTML抓取应用程序。对Nokogiri::HTML(open())的调用是一个同步阻塞调用,需要1秒才能返回,我有100,000多个页面要访问,所以我试图运行多个线程来解决这个问题。有更好的方法吗?classToolsController0)app.website=array.join(',')putsapp.websiteelseapp.website="NONE"endapp.saveapps=Apps.order("

  5. ruby-on-rails - 使用 config.threadsafe 时从 lib/加载模块/类的正确方法是什么!选项? - 2

    我一直致力于让我们的Rails2.3.8应用程序在JRuby下正确运行。一切正常,直到我启用config.threadsafe!以实现JRuby提供的并发性。这导致lib/中的模块和类不再自动加载。使用config.threadsafe!启用:$rubyscript/runner-eproduction'pSim::Sim200Provisioner'/Users/amchale/.rvm/gems/jruby-1.5.1@web-services/gems/activesupport-2.3.8/lib/active_support/dependencies.rb:105:in`co

  6. ruby-on-rails - 从应用程序中自定义文件夹内的命名空间自动加载 - 2

    我们目前正在为ROR3.2开发自定义cms引擎。在这个过程中,我们希望成为我们的rails应用程序中的一等公民的几个类类型起源,这意味着它们应该驻留在应用程序的app文件夹下,它是插件。目前我们有以下类型:数据源数据类型查看我在app文件夹下创建了多个目录来保存这些:应用/数据源应用/数据类型应用/View更多类型将随之而来,我有点担心应用程序文件夹被这么多目录污染。因此,我想将它们移动到一个子目录/模块中,该子目录/模块包含cms定义的所有类型。所有类都应位于MyCms命名空间内,目录布局应如下所示:应用程序/my_cms/data_source应用程序/my_cms/data_ty

  7. 安卓apk修改(Android反编译apk) - 2

    最近因为项目需要,需要将Android手机系统自带的某个系统软件反编译并更改里面某个资源,并重新打包,签名生成新的自定义的apk,下面我来介绍一下我的实现过程。APK修改,分为以下几步:反编译解包,修改,重打包,修改签名等步骤。安卓apk修改准备工作1.系统配置好JavaJDK环境变量2.需要root权限的手机(针对系统自带apk,其他软件免root)3.Auto-Sign签名工具4.apktool工具安卓apk修改开始反编译本文拿Android系统里面的Settings.apk做demo,具体如何将apk获取出来在此就不过多介绍了,直接进入主题:按键win+R输入cmd,打开命令窗口,并将路

  8. ruby-on-rails - 您希望看到哪些 Rails 插件? - 2

    您认为可以作为插件很好地存在于您的Rails应用程序中必须实现的哪些行为?您过去曾搜索过哪些插件功能但找不到?哪些现有的Rails插件可以改进或扩展,如何改进或扩展? 最佳答案 我希望在管理界面中看到一个引擎插件,它提供了应用程序中所有模型的仪表板摘要,以及可配置的事件图表。 关于ruby-on-rails-您希望看到哪些Rails插件?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questio

  9. ruby - 在 Ruby 中动态创建数组 - 2

    有没有办法在Ruby中动态创建数组?例如,假设我想遍历用户输入的书籍数组:books=gets.chomp用户输入:"TheGreatGatsby,CrimeandPunishment,Dracula,Fahrenheit451,PrideandPrejudice,SenseandSensibility,Slaughterhouse-Five,TheAdventuresofHuckleberryFinn"我把它变成一个数组:books_array=books.split(",")现在,对于用户输入的每一本书,我想用Ruby创建一个数组。伪代码来做到这一点:x=0books_array.

  10. ruby - 是否可以将 IRB 提示配置为动态更改? - 2

    我想在IRB中浏览文件系统并让提示更改以反射(reflect)当前工作目录,但我不知道如何在每个命令后进行提示更新。最终,我想在日常工作中更多地使用IRB,让bash溜走。我在我的.irbrc中试过这个:require'fileutils'includeFileUtilsIRB.conf[:PROMPT][:CUSTOM]={:PROMPT_N=>"\e[1m:\e[m",:PROMPT_I=>"\e[1m#{pwd}>\e[m",:PROMPT_S=>"FOO",:PROMPT_C=>"\e[1m#{pwd}>\e[m",:RETURN=>""}IRB.conf[:PROMPT_MO

随机推荐