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下),文件结构入下图所示:
参考代码如下:
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注册信息。
注意三个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传送门