您的位置:首页 > 移动开发 > Android开发

android 通过代理activity的方式实现插件化

2017-07-18 16:18 543 查看

前言:

一直以来就对插件化这技术推崇已久,在去年也写过两篇关于插件化基础的文章:
Java中的ClassLoader 动态加载机制
Android中的动态加载
都是关于classLoader如何加载外部apk中的代码,在"android中的动态加载"这篇博客末尾,提了下如何打开插件的activity,所以这篇文章就是谈如何通过代理Activity实现打开插件中的activity的。由于插件化的技术现在相对而言已经很成熟了,很多公司都已经运用在项目中,网上也有许多关于插件化的介绍,只是看到网上很多资料都写的不是很完整(可能我技术层面还没到达),只说了一些片面的代码或者是一些基础原理,并没有真正意义上的demo实现。原理是这么回事,但是真正到自己实现起来就发现知道原理并不能立马做好项目。网上也有许多demo是错误的,对初学者学习插件化造成了很大的困扰,在参考了许多资料之后,才决定写一篇关于如何实现插件化的文章。(这里对于宿主如何访问插件的资源、dexclassloader等就不再叙述了,这些基础知识网上有很多资料都有介绍)

参考资料:https://github.com/singwhatiwanna/dynamic-load-apk

实现代理activity的两种方式:(如何管理插件activity的生命周期)

1.通过反射方式实现代理activity。

2.通过接口方式实现代理activity

接下来就会分别使用以上两种方式实现插件化,先来看下运行结果。



了解过apk的安装都知道或者说了解PackageManagerService的都应该清楚,(如果不知道的可以看下我这篇博客:android apk安装过程源码解析)在安装过程中apk会被解析,然后apk中的信息比如四大组件都会被封装到一个package对象中,这样我们启动一个activity就只要从这个package对象中获取这个activity的信息就可以启动了。但是要启动一个插件中的activity(这个apk在外部,并没有被注册到我们的package中),这样的方式就行不通了。所以就需要在宿主activity中创建一个代理activity来管理插件activity的生命周期,而我们插件中的activity就相当于一个普通的类了。

先看下插件项目的目录结构



可以看到我们的插件项目仅仅2个Activity以及一个接口,(这个接口是用来)其实从运行结果也能看到,插件就一个需要显示的activity,并且内容也仅仅是一个textview。接下来就来看下我们插件中的3个类的内容。

PluginInterface:
声明activity的生命周期方法,通过接口的方式来管理activity的生命周期,避免反射减少性能消耗。
package com.example.lujianxin.proxy;

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

/**
* Created by lujianxin on 2017/7/14.
*/

public interface PluginInterface {
public void onStart();
public void onResume();
public void onPause();
public void onStop();
public void onDestroy();
public void onCreate(Bundle savedInstanceState);
public void setProxy(Activity proxyActivity);

}

这里仅仅只为demo服务,所以没有写另外的方法了,如果有需要可以根据自己的项目来添加.

BaseActivity:
package com.example.lujianxin.proxy;

import android.app.Activity;
import android.os.Bundle;
import android.support.annotation.LayoutRes;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;

/**
* Created by lujianxin on 2017/7/13.
*/

public class BaseActivity extends Activity implements PluginInterface {
public Activity mProxyActivity;

@Override
public void onCreate(Bundle savedInstanceState) {
}

@Override
public void setContentView(@LayoutRes int layoutResID) {
if(mProxyActivity != null && mProxyActivity instanceof Activity){
mProxyActivity.setContentView(layoutResID);
TextView tv = (TextView) mProxyActivity.findViewById(R.id.proxy_tv);
tv.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Toast.makeText(mProxyActivity, "this is proxy activity!", Toast.LENGTH_LONG).show();
}
});
}
}

@Override
public void onStart() {
}

@Override
public void onResume() {
}

@Override
public void onPause() {
}

@Override
public void onStop() {
}

@Override
public void onDestroy() {
}

@Override
public void setProxy(Activity proxyActivity) {
mProxyActivity = proxyActivity;
}

}


这个类也简单,接受一个activity代理对象,通过这个代理对象来管理插件中的activity生命周期。然后重写setcontentview()方法,让这个代理对象来执行这个setcontentview()方法。并且实现接口为"接口方式管理activity生命周期"提供服务的。

MainActivity:
package com.example.lujianxin.proxy;

import android.os.Bundle;
import android.util.Log;

public class MainActivity extends BaseActivity implements PluginInterface {

private static final String TAG = "MainActivity";

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

@Override
public void onStart() {
Log.d(TAG, "plugin-onStart");
}

@Override
public void onResume() {
Log.d(TAG, "plugin-onResume");
}

@Override
public void onPause() {
Log.d(TAG, "plugin-onPause");
}

@Override
public void onStop() {
Log.d(TAG, "plugin-onStop");
}

@Override
public void onDestroy() {
Log.d(TAG, "plugin-onDestroy");
}

}

MainActivity仅仅只是在调用oncreate的时候执行父类的setcontentview()方法。本来在这之前我是没准备写BaseActivity的,所以这里MainActivity也实现了接口,这里可以去掉。

可以看到,插件还是很简单的,只是我们在应用的时候,应该把插件中的activity看成是一个类,而不是android中的activity。

宿主目录结构:



从上面可以看到,宿主有4个类加一个接口:
1.MainActivity就是我们结果展示的两个button页面
2.BaseActivity中处理插件资源的访问
3.PlugProxyActivity中通过接口的方式管理插件activity的生命周期。
4.ProxyActivity中通过反射的方式管理插件activity的生命周期。
5.PluginInterface和我们插件的一样,这样包名也需要一样。

在具体分析宿主之前,要说下的是我这里没有把插件放到服务器上,直接通过复制到/data/data/宿主包名/cache下。



可以通过以上命令来把我们的apk放入到该目录下,这里测试的话用模拟器就行了,如果是真机的话,就需要root了。

先来看下BaseActivity:
package com.example.lujianxin.myapplication;

import android.app.Activity;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.AssetManager;
import android.content.res.Resources;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;

import java.lang.reflect.Method;

/**
* Created by lujianxin on 2017/7/13.
*/

public class BaseActivity extends Activity {

protected AssetManager mAssetManager;
protected Resources mResources;
protected Resources.Theme mTheme;
private ActivityInfo mActivityInfo;
private PackageInfo packageInfo;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
}
//访问插件中的资源
protected void loadResources(String dexPath, Activity mProxyActivity) {
initializeActivityInfo(dexPath);
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, dexPath);
mAssetManager = assetManager;
} catch (Exception e) {
Log.i("inject", "loadResource error:"+Log.getStackTraceString(e));
e.printStackTrace();
}
Resources superRes = super.getResources();
superRes.getDisplayMetrics();
superRes.getConfiguration();

if (mActivityInfo.theme > 0) {
mProxyActivity.setTheme(mActivityInfo.theme);
}
Resources.Theme superTheme = mProxyActivity.getTheme();
mResources = new Resources(mAssetManager, superRes.getDisplayMetrics(),superRes.getConfiguration());
mTheme = mResources.newTheme();
mTheme.setTo(superTheme);
try {
mTheme.applyStyle(mActivityInfo.theme, true);
} catch (Exception e) {
e.printStackTrace();
}
}

@Override
public AssetManager getAssets() {
return mAssetManager == null ? super.getAssets() : mAssetManager;
}

@Override
public Resources getResources() {
return mResources == null ? super.getResources() : mResources;
}

private void initializeActivityInfo(String dexPath) {
packageInfo = getApplicationContext().getPackageManager().getPackageArchiveInfo(dexPath,
PackageManager.GET_ACTIVITIES | PackageManager.GET_SERVICES);
if ((packageInfo.activities != null) && (packageInfo.activities.length > 0)) {
//            if (mClass == null) {
//                mClass = packageInfo.activities[0].name;
//            }

//Finals 修复主题BUG
int defaultTheme = packageInfo.applicationInfo.theme;
for (ActivityInfo a : packageInfo.activities) {
//                if (a.name.equals(mClass)) {
mActivityInfo = a;
// Finals ADD 修复主题没有配置的时候插件异常
if (mActivityInfo.theme == 0) {
if (defaultTheme != 0) {
mActivityInfo.theme = defaultTheme;
} else {
if (Build.VERSION.SDK_INT >= 14) {
mActivityInfo.theme = android.R.style.Theme_DeviceDefault;
} else {
mActivityInfo.theme = android.R.style.Theme;
}
}
//                    }
}
}

}
}

@Override
public Resources.Theme getTheme() {
return mTheme == null ? super.getTheme() : mTheme;
}

}

这里重点要关注的是Theme,与网上的很多资料不同,这里是直接拿DL框架中的代码过来的,如果用网上的资料来进行资源处理,会造成bug。

MainActivity:
package com.example.lujianxin.myapplication;

import android.app.Activity;
import android.content.Intent;
import android.os.Environment;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

public class MainActivity extends Activity {
private static final String TAG = "MainActivity";
private Button interfaceBtn, reflexBtn;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toast.makeText(this, Environment.getExternalStorageDirectory().getAbsolutePath(), Toast.LENGTH_LONG).show();
interfaceBtn = (Button) findViewById(R.id.interface_btn);
reflexBtn = (Button) findViewById(R.id.reflex_btn);

reflexBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//通过反射方式处理
Intent intent = new Intent(MainActivity.this, ProxyActivity.class);
startActivity(intent);
}
});

interfaceBtn.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
//通过接口的方式处理
Intent intent = new Intent(MainActivity.this, PlugProxyActivity.class);
startActivity(intent);
}
});
}

}


这里就起一个跳转的作用。

PlugProxyActivity:

package com.example.lujianxin.myapplication;
import android.os.Bundle;
import android.util.Log;
import com.example.lujianxin.proxy.PluginInterface;
import java.io.File;
import java.lang.reflect.Constructor;
import dalvik.system.DexClassLoader;

/**
* Created by lujianxin on 2017/7/14.
*/

public class PlugProxyActivity extends BaseActivity {
private static final String TAG = "ProxyActivity";

private static final String  dexPath = "/data/data/com.example.lujianxin.myapplication/cache/proxy1.apk";
private static final String activityName = "com.example.lujianxin.proxy.MainActivity";
// dex解压之后存放的路径,如果是一个固定的路径运行程序的时候会报错:optimizedDirectory not readable/writable
private File dexOutputDir;

private Class<?> clazz;

private PluginInterface mPluginInterface;

@Override
protected void onCreate(Bundle savedInstanceState) {
loadResources(dexPath, this);
super.onCreate(savedInstanceState);
try {
DexClassLoader loader = initClassLoader();

//动态加载插件Activity
clazz = loader.loadClass(activityName);
Constructor<?> localConstructor = clazz.getConstructor(new Class[] {});
//拿到我们的activity 强转成它实现的接口PluginInterface
mPluginInterface = (PluginInterface) localConstructor.newInstance(new Object[] {});

mPluginInterface.setProxy(this);

Bundle bundle = new Bundle();
mPluginInterface.onCreate(bundle);

} catch (Exception e) {
Log.d(TAG, e.toString());
}
}

private DexClassLoader initClassLoader(){

dexOutputDir = getApplicationContext().getDir("dex", 0);
String filesDir = this.getCacheDir().getAbsolutePath();
String libPath = filesDir + File.separator +"proxy1.apk";
Log.i(TAG, "file-exist-PlugProxyActivity:"+new File(libPath).exists());

DexClassLoader loader = new DexClassLoader(libPath, dexOutputDir.getAbsolutePath(), null, getClass().getClassLoader());
return loader;
}

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

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

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

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

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

这里的重点就是,插件与宿主都存在一个包名相同的接口,同时插件activity要实现这个接口,然后我们通过classloader加载到插件中的activity的时候,就可以强壮成这个接口,然后通过这个接口来管理插件中的activity的生命周期。

ProxyActivity:

package com.example.lujianxin.myapplication;

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

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

import dalvik.system.DexClassLoader;

/**
* Created by lujianxin on 2017/7/12.
*/

public class ProxyActivity extends BaseActivity {

private static final String TAG = "ProxyActivity";

private static final String  dexPath = "/data/data/com.example.lujianxin.myapplication/cache/proxy1.apk";
private static final String activityName = "com.example.lujianxin.proxy.MainActivity";
// dex解压之后存放的路径,如果是一个固定的路径运行程序的时候会报错:optimizedDirectory not readable/writable
private File dexOutputDir;
private Class<?> clazz;

private Object pluginActivity;

@Override
protected void onCreate(Bundle savedInstanceState) {
loadResources(dexPath, this);
super.onCreate(savedInstanceState);
try {
DexClassLoader loader = initClassLoader();

//获取插件activity实例
clazz = loader.loadClass(activityName);
Constructor<?> localConstructor = clazz.getConstructor(new Class[] {});
pluginActivity = localConstructor.newInstance(new Object[] {});

//将代理对象设置给插件Activity
Method setProxy = clazz.getMethod("setProxy",new Class[] { Activity.class });
setProxy.setAccessible(true);
setProxy.invoke(pluginActivity, new Object[] { this });

//调用它的onCreate方法
Method onCreate = clazz.getDeclaredMethod("onCreate",
new Class[] { Bundle.class });
onCreate.setAccessible(true);
onCreate.invoke(pluginActivity, new Object[] { new Bundle() });

} catch (Exception e) {
Log.i(TAG, e.toString());
}
}

private DexClassLoader initClassLoader(){
dexOutputDir = getApplicationContext().getDir("dex", 0);
String filesDir = this.getCacheDir().getAbsolutePath();
String libPath = filesDir + File.separator +"proxy1.apk";
Log.i(TAG, "file-exist-proxyActivity:"+new File(libPath).exists());

DexClassLoader loader = new DexClassLoader(libPath, dexOutputDir.getAbsolutePath(), null, getClass().getClassLoader());
return loader;
}

@Override
protected void onDestroy() {
super.onDestroy();
;
try {
Method method = clazz.getDeclaredMethod("onDestroy");
method.setAccessible(true);
method.invoke(pluginActivity, new Object[]{});
} catch(Exception e){
Log.d(TAG, e.toString());
}

}

@Override
protected void onPause() {
super.onPause();
try {
Method method = clazz.getDeclaredMethod("onPause");
method.setAccessible(true);
method.invoke(pluginActivity, new Object[]{});
} catch(Exception e){
Log.d(TAG, e.toString());
}
}

@Override
protected void onResume() {
super.onResume();
try {
Method method = clazz.getDeclaredMethod("onResume");
method.setAccessible(true);
method.invoke(pluginActivity, new Object[]{});
} catch(Exception e){
Log.d(TAG, e.toString());
}
}

@Override
protected void onStart() {
super.onStart();
try {
Method method = clazz.getDeclaredMethod("onStart");
method.setAccessible(true);
method.invoke(pluginActivity, new Object[]{});
} catch(Exception e){
Log.d(TAG, e.toString());
}
}

@Override
protected void onStop() {
super.onStop();
try {
Method method = clazz.getDeclaredMethod("onStop");
method.setAccessible(true);
method.invoke(pluginActivity, new Object[]{});
} catch(Exception e){
Log.d(TAG, e.toString());
}
}

}

这里的实现就简单明了了,通过反射机制来调起插件activity的生命周期方法。

以上就是整个demo的实现,末尾会添加demo的链接。通过代理activity实现插件化,我们应该把插件中的所有类都看成是没有android特有的生命周期的类,运行的还是我们自己宿主中的activity,只是我们宿主activity的生命周期中运行的内容就是插件中的内容。前段时间看到有篇博客说的蛮不错的,这种方式就相当于牵线木偶,而我们的插件就相当于这个木偶,我们加载插件就相当于操作这个木偶;这种方式并不需要过多的涉及底层,另外还有一种Hook的方式实现-360DroidPlugin,想了解的可以去看看,对于framework层的了解有很大帮助。

插件化demo下载
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: