Android插件化——加载其他APP页面


因工作需要,整理下插件化开发的demo,方便交流使用。

1.分析

插件化开发开发时将整个app拆分,包括一个宿主和多个插件,每个插件都是一个apk(组件化每个组件是lib),最终打包的时候将宿主apk和插件apk分开或者联合打包。

使用场景就是原生APP-A在不安装原生APP-B的情况下加载其页面。

其实有很多APP都是采用这种方式,比如现在百度一下,烂大街的支付宝加载淘票票。。。那怎么判断是否为插件加载呢?
1.没有明显过程动画,不需要安装apk。如果采用Intent启动的方式,会有明显过场动画。
2.判断是否为webview加载。开启:开发者选项——显示边界布局,如果没有代表布局的彩色栅格,即为原生加载。

2.优点

  • 宿主和插件分开编译。
  • 并行开发,节约时间。
  • 动态更新插件。
  • 按需下载插件模块(第一次下载较慢)。
  • 方法分离为多个APP,避免65535.

3.详细过程

废话不多说,边看代码边解释。最后实现场景是APP-A加载APP-B的Activity

3.1 标准化加载接口

既然需要A与B能够连接(通信),需要定义一个接口。因为是加载Activity,接口当然就要符合Activity的生命周期。同时,加载布局需要Context的支持,所以也需要Context的注入。
新建一个lib,创建一个如上接口。(方便调试,所有的lib与module都在一个Project下),文件结构入下图所示:包含Interface的lib路径
参考代码如下:

package com.heima.plugs;

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

/**
 * 符合加载标准(Activity生命周期 + Context注入)
 */
public interface PlugInterface {

    void onCreate(Bundle saveInstance);
    void onStart();
    void onResume();
    void onRestart();
    void onDestroy();
    void onStop();
    void onPause();

    /**
     * 注入context
     * @param context
     */
    void attachContext(Activity context);
}

3.2待加载的APP-B

新建module作为单独的被加载APP。
由于B中的Activity需要被加载,所以选择写一个基类BaseActivity,实现Interface,同时注意注入的Context对象,所以还要重写与上下文对象相关的方法。让工程中Activity继承BaseActivity,方便操作。Module结构入下图所示:在这里插入图片描述
从图中可以看出,module中除了BaseActivity外,还分别创建了3个Activity等待APP-A加载。当然,这些Activity都继承BaseActivity。
这时候我们需要注意的是AndroidManifest.xml中的Activity注册信息。AndroidManifest.xml
注意三个Activity的注册顺序,这个跟下面所讲的在A中加载有关系。这个顺序,跟在A中获得的Activity队列相关。后面会详细描述。

上代码:

package com.heima.otherapp;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;

import com.heima.plugs.PlugInterface;

public class BaseActivity extends Activity implements PlugInterface {
    public  static  final  String TAG="BaseActivity";
    protected Activity thatActivity;

    /**
     * 注入自己的上下文
     * 如果为空 使用父类
     *
     * @param layoutResID
     */
    @Override
    public void setContentView(int layoutResID) {
        if (thatActivity == null) {
            super.setContentView(layoutResID);
        } else {
            thatActivity.setContentView(layoutResID);
        }
    }

    @Override
    public void setContentView(View view) {
        if (thatActivity == null) {
            super.setContentView(view);
        } else {
            thatActivity.setContentView(view);
        }
    }

    @Override
    public void setContentView(View view, ViewGroup.LayoutParams params) {
        if (thatActivity == null) {
            super.setContentView(view, params);
        } else {
            thatActivity.setContentView(view, params);
        }
    }

    @Override
    public LayoutInflater getLayoutInflater() {
        if (thatActivity == null) {
            return super.getLayoutInflater();
        } else {
            return thatActivity.getLayoutInflater();
        }
    }

    @Override
    public Window getWindow() {
        if (thatActivity == null) {
            return super.getWindow();
        } else {
            return thatActivity.getWindow();
        }
    }

    @Override
    public View findViewById(int id) {
        if (thatActivity == null) {
            return super.findViewById(id);
        } else {
            return findViewById(id);
        }
    }


    @Override
    public ClassLoader getClassLoader() {
        if (thatActivity == null) {
            return super.getClassLoader();
        } else {
            return getClassLoader();
        }

    }

    @Override
    public WindowManager getWindowManager() {
        if (thatActivity == null) {
            return super.getWindowManager();
        } else {
            return thatActivity.getWindowManager();
        }
    }


    @Override
    public ApplicationInfo getApplicationInfo() {
        if (thatActivity == null) {
            return super.getApplicationInfo();
        } else {
            return thatActivity.getApplicationInfo();
        }
    }

    @Override
    public void attachContext(Activity context) {
        thatActivity = context;
    }
    
    public void onCreate(Bundle savedInstanceState) { }
    public void onStart() { }
    public void onResume() { }
    public void onRestart() { }
    public void onPause() { }
    public void onStop() {  }
    public void onDestroy() { }
    public void onSaveInstanceState(Bundle outState) { }

    public boolean onTouchEvent(MotionEvent event) {
        return false;
    }

    public void onBackPressed() {
        if (thatActivity == null) {
            super.onBackPressed();
        } else {
            thatActivity.onBackPressed();
        }
    }

    @Override
    public void finish() {
        if (thatActivity == null) {
            super.finish();
        } else {
            thatActivity.finish();
        }
    }

    /**
     * 注意上下文对象 thatActivity
     * @param intent
     */
    @Override
    public void startActivity(Intent intent) {
        if (thatActivity == null) {
            super.startActivity(intent);
        } else {
            intent.putExtra("className", intent.getComponent().getClassName());
            thatActivity.startActivity(intent);
        }
    }
    
}

待加载页面:

/**
 * 待加载app主界面
 * 此app没有安装,仅存.apk文件在内存卡
 */
public class MainActivity extends BaseActivity {

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

    public void start(View view) {
        //使用注入的context
        startActivity(new Intent(thatActivity, SecondActivity.class));
    }
}

其他Activity一样。
Module完成后,把其push到手机的sdcard中,等待加载。

3.3 APP-A 主加载工程

思路:

  • 1.获取读写内存卡权限。
  • 2.通过内存卡路径获取B的相关文件。
  • 3.承载页面。
    文件创建如图所示:
    在这里插入图片描述

3.3.1 加载工具类PlugManager

使用DexClassLoader+AssetManager获取外部APK资源。代码中有详细注解:

package com.heima.teststart;

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

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

import dalvik.system.DexClassLoader;

/**
 * DexClassLoader加载第三方app
 */
public class PlugManager {
    public  static  final  String TAG="PlugManager";
    private static PlugManager ourInstance;
    private Context context;
    private DexClassLoader pluginDexClassLoader;
    private Resources pluginResources;
    private PackageInfo pluginPackageArchiveInfo;
    private String entryActivityName;

    private PlugManager() { }

    public static PlugManager getInstance() {
        if (ourInstance == null) {
            ourInstance = new PlugManager();
        }
        return ourInstance;
    }

    public void setContext(Context context) {
        this.context = context.getApplicationContext();
    }

    /**
     * 获取Plugin的字节码文件对象
     * 加载外部apk,重写getDexClassLoader()与getResources()
     * @param dexPath Plugin的路径
     */
    public void loadApk(String dexPath) {
        /*  optimizedDirectory   Plugin的缓存路径
         *  libraryPath          可以为null
         *  parent              为父类加载器
         */
        File dexOutFile = context.getDir("dex", Context.MODE_PRIVATE);
        pluginDexClassLoader = new DexClassLoader(dexPath, dexOutFile.getAbsolutePath(), null, context.getClassLoader());

        // 获取包名
        PackageManager packageManager = context.getPackageManager();
        pluginPackageArchiveInfo = packageManager.getPackageArchiveInfo(dexPath, PackageManager.GET_ACTIVITIES);
        //activity集合跟App-B的Manifest中注册的activity有关 顺序也有关
        entryActivityName = pluginPackageArchiveInfo.activities[1].name;
        for (int i=0;i<pluginPackageArchiveInfo.activities.length;i++){
            Log.e(TAG, pluginPackageArchiveInfo.activities[i].name);
        }

      /*  实例化resources
        Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) */
        AssetManager assets = null;
        try {
            assets = AssetManager.class.newInstance();
            Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
            addAssetPath.invoke(assets, dexPath);
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchMethodException e) {
            e.printStackTrace();
        } catch (InvocationTargetException e) {
            e.printStackTrace();
        }
        //获取Plugin的Resources
        pluginResources = new Resources(assets, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());
    }

    public PackageInfo getPluginPackageArchiveInfo() {
        return pluginPackageArchiveInfo;
    }

    public DexClassLoader getPluginDexClassLoader() {
        return pluginDexClassLoader;
    }

    public Resources getPluginResources() {
        return pluginResources;
    }

    public String getEntryActivityName() {
        return entryActivityName;
    }

}

这里一定要注意pluginPackageArchiveInfo.activities[i].name遍历这个数组你会发现,遍历的顺序跟B中AndroidManifest.xml的Activity注册顺序有关。

3.3.2 页面加载器 ProxyActivity

用来承载B中待加载Activity的内容。

package com.heima.teststart;

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

import com.heima.plugs.PlugInterface;


/**
 * 承载页---加载第三方activity页面
 */
public class ProxyActivity extends Activity {

    private PlugInterface pluginInterface;

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //待启动的第三方Activity
        String className = getIntent().getStringExtra("className");
        try {
            //加载该Activity的字节码对象
            Class<?> aClass = PlugManager.getInstance().getPluginDexClassLoader().loadClass(className);
            //创建该Activity的实例
            Object newInstance = aClass.newInstance();
            //程序健壮性检查
            if (newInstance instanceof PlugInterface) {
                pluginInterface = (PlugInterface) 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();
        }
    }

    /**
     * 重新,通过className拿到类名
     *
     * @return
     */
    @Override
    public ClassLoader getClassLoader() {
        return PlugManager.getInstance().getPluginDexClassLoader();
    }


    /**
     * 注意:三方调用拿到对应加载的三方Resources
     *
     * @return
     */
    @Override
    public Resources getResources() {
        return PlugManager.getInstance().getPluginResources();
    }

    @Override
    public void startActivity(Intent intent) {
        Intent intent1 = new Intent(this, ProxyActivity.class);
       String className = intent.getStringExtra("className");
        //intent1.putExtra("className", intent.getComponent().getClassName());
        intent1.putExtra("className",className);
        super.startActivity(intent1);
    }

    @Override
    public void onStart() {
        if (pluginInterface != null)
            pluginInterface.onStart();
        super.onStart();
    }

    @Override
    public void onResume() {
        if (pluginInterface != null)
            pluginInterface.onResume();
        super.onResume();
    }

    @Override
    public void onPause() {
        if (pluginInterface != null)
            pluginInterface.onPause();
        super.onPause();
    }

    @Override
    public void onRestart() {
        if (pluginInterface != null)
            pluginInterface.onRestart();
        super.onRestart();
    }

    @Override
    public void onStop() {
        if (pluginInterface != null)
            pluginInterface.onStop();
        super.onStop();
    }

    @Override
    public void onDestroy() {
        if (pluginInterface != null)
            pluginInterface.onDestroy();
        super.onDestroy();
    }

}

3.3.3 启动页面 MainActivity

通过PlugManager获取相关内容,传入ProxyActivity进行加载操作。

package com.heima.teststart;

import android.Manifest;
import android.content.Intent;
import android.os.Environment;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityCompat;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.view.View;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        loadApk();
    }

    public void loadApk() {
        //使用运行时权限
        ActivityCompat.requestPermissions(this,
                new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}, 100);
    }

    public void startApk(View view) {
        Intent intent = new Intent(this, ProxyActivity.class);
        String otherApkMainActivityName = PlugManager.getInstance().getEntryActivityName();
        intent.putExtra("className", otherApkMainActivityName);
        startActivity(intent);
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        PlugManager.getInstance().setContext(this);
        //传入APP-B的绝对路径
        PlugManager.getInstance().loadApk(Environment.getExternalStorageDirectory().getAbsolutePath()+"/otherapp-debug.apk");
    }

}

OK,完成。
附上工程连接,方便下载。GitHub传送门

猜你喜欢

转载自blog.csdn.net/ma598214297/article/details/88630310