Android插件开发 —— 通过预注册方式打开activity(记录我踩过的坑)
插件开发的原理简单的说就是将插件apk合并到宿主的ClassLoader中。我先简单说下如何使用插件中的资源,因为预注册时有些坑就跟这个有关系。
要使用apk中的资源,我们首先想到有个Resources就好了,先看下Resources的构造方法:
public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config)
{
this(assets, metrics, config, null);
}
所以接下来我们就要获取一个AssetManager的对象,通过反射,如下(其中dexPath为插件apk的路径):
private AssetManager createAssetManager(
String dexPath )
{
try
{
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getMethod( "addAssetPath" , String.class );
addAssetPath.invoke( assetManager , dexPath );
return assetManager;
}
catch( Exception e )
{
e.printStackTrace();
return null;
}
}
现在AssetManager也有了,我们就可以生成Resources(下面的mApplicationContext是宿主的ApplicationContext)。
private Resources createResources(
AssetManager assetManager )
{
Resources superRes = mApplicationContext.getResources();
Resources resources = new Resources( assetManager , superRes.getDisplayMetrics() , superRes.getConfiguration() );
return resources;
}
然后我们可以自定义一个context,来管理我们新生成的这个AssetManager和Resources。
public class DLPluginContext extends ContextWrapper
{
protected AssetManager mAssetManager;
protected Resources mResources;
protected Theme mTheme;
protected ClassLoader classLoader;
//pluginPackageName是插件apk的包名
public DLPluginContext(
Context containerContext,String pluginPackageName )
{
super( containerContext );
String pluginPath = DLUtils.getPluginPath( containerContext , pluginPackageName );
mAssetManager = createAssetManager(pluginPath );
mResources = createResources(mAssetManager );
int mThemeResource = DLUtils.selectDefaultTheme(0,
containerContext.getApplicationInfo().targetSdkVersion);
if (mTheme == null) {
mTheme = mResources.newTheme();
}
mTheme.applyStyle(mThemeResource, true);
classLoader = pluginPackage.classLoader;
}
@Override
public Resources getResources()
{
if(this.mResources != null){
return this.mResources;
}
return super.getResources();
}
@Override
public AssetManager getAssets()
{
// TODO Auto-generated method stub
if(this.mAssetManager != null)
return this.mAssetManager;
return super.getAssets();
}
@Override
public Theme getTheme()
{
// TODO Auto-generated method stub
if(this.mTheme != null)
return this.mTheme;
return super.getTheme();
}
@Override
public ClassLoader getClassLoader()
{
if(this.classLoader != null)
return classLoader;
return super.getClassLoader();
}
}
接下来:
android插件化启动activity的方式有两种方式
1、代理方式:参见“木质的旋律”大大的博客
2、预注册的方式:参见“木质的旋律”大大的博客
首先我们的宿主apk中有个管理类DLHost,其中有一个DLPlugin的对象,DLPlugin的是一个抽象类,具体实现是在插件中。DLHost的initPlugin()方法实现dexClassLoader的合并,并通过类名的反射获取到一个DLPlugin的对象。这样就实现了宿主和插件的通信。
public abstract class DLHost
{
public static final String SUFFIX = ".apk";
protected Context containerContext;
protected DLPlugin plugin;
public DLHost(
Context containerContext )
{
this.containerContext = containerContext;
}
public Context getContext()
{
return containerContext;
}
public void start(
String packageName ,
String pluginClassName )
{
try
{
initPlugin( packageName , pluginClassName );
}
catch( Throwable e )
{
//Log.i( "" , " e.printStackTrace(); " );
e.printStackTrace();
}
}
private void initPlugin(
String packageName ,
String pluginClassName )
{
// Log.d( "update" , "pluginPath 0" );
if( plugin != null )
return;
checkPluginFile( packageName );
String pluginPath = DLUtils.getPluginPath( containerContext , packageName );
PackageInfo packageInfo = DLUtils.getPackageInfo( containerContext , pluginPath );
if( packageInfo != null )
{
DLPluginManager mDLPluginManager = DLPluginManager.getInstance( containerContext );
mDLPluginManager.loadApk( pluginPath );
DLPluginPackage pluginPackage = mDLPluginManager.getPackage( packageName );
if( pluginPackage == null )
{
return;
}
Class<?> clazz = DLUtils.loadPluginClass( pluginPackage.classLoader , pluginClassName );
if( clazz == null )
{
return;
}
DLPlugin instance;
try
{
Constructor<?> m = null;
m = clazz.getConstructor( new Class[]{ DLHost.class } );
instance = (DLPlugin)m.newInstance( new Object[]{ this } );
plugin = instance;
}
catch( Exception e )
{
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
TestHost继承DLHost,宿主MainActivity的button点击时,先完成dexclassloader的合并,然后启动activity。
public class MainActivity extends Activity
{
@Override
protected 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 )
{
// TODO Auto-generated method stub
TestHost.getInstance( getApplicationContext() );
Intent it = new Intent();
it.setClassName( MainActivity.this , "com.zjp.example.testplugin.PluginActivity" );
startActivity( it );
}
} );
}
}
“com.zjp.example.testplugin.PluginActivity”是插件中的activity,插件apk中的这个activity的内容如下:
public class PluginActivity extends Activity
{
@Override
protected void onCreate(
Bundle savedInstanceState )
{
super.onCreate( savedInstanceState );
TextView tv = new TextView( this );
tv.setText( "这是插件中的activity" );
setContentView( tv );
}
}
看到这里,大家可以看到,我在插件的activity中并没有使用到插件apk中的资源,如果不使用插件apk的这个资源,所有的view都靠new出来,到这里就可以结束啦。但正常的大家一般不会自己为难自己的,界面还是通过布局文件实现的。
下面开始在插件中使用资源文件。
我想通过插件中DLPluginContext生成LayoutInflater.from( pluginContext ).inflate( R.layout.xxx , null );一个view,然后通过setContentView(view)就实现了引用插件中的资源。
说到这里,就来看看我们的DLPlugin(上面DLHost中有一个DLPlugin的对象)。
public abstract class DLPlugin
{
protected DLHost host;
protected Context pluginContext;
public DLPlugin(
DLHost host )
{
this.host = host;
pluginContext = new DLPluginContext( host.getContext() , getPackageName() );
}
}
DLPlugin中生成了DLPluginContext的对象,而DLPlugin我们上面已经通过DLHost反射生成了实例,这样我们就可以直接用了。
但是却有空指针,plugin对象为空。
如下图1的log堆栈中,在host也就是宿主中通过反射调用了plugin(插件)种TestPlugin(继承DLPlugin)的初始化方法,并且赋值给一个静态变量instance,如下图2,但是在plugin中我直接调用这个TestPlugin.getInstance()静态方法却返回的是一个null,见如图1的最后一行log。
图1
图2
这个问题到现在我也没有搞明白是为什么,静态变量在同一个进程中(我打印过进程id,id值相等)不是可以大家一起用的么?如果有谁明白,请回复一下我。
public class PluginActivity extends Activity
{
@Override
protected void onCreate(
Bundle savedInstanceState )
{
super.onCreate( savedInstanceState );
Context context = new DLPluginContext( this , "com.zjp.example.testplugin" );
View view = (ViewGroup)LayoutInflater.from( context ).cloneInContext( context ).inflate( R.layout.activity_main , null );
setContentView( view );
TextView tv = (TextView)findViewById( R.id.helloworld );
tv.setText( "这是插件布局的中的textview" );
}
}
记住:这里通过LayoutInflater来生成布局一定要执行cloneInContext( context ),不然不能生成view。
你以为这篇流水账到这里就结束了么,还没有,请看官们接着往下看。
我在我的布局文件中加入了一个webview,也可以实现。
这里我遇到webview播放网络视频的问题,点击全屏按钮,能全屏播放。在4.2的机器上出现了视频全屏后不显示播放、暂停的按钮。但是另写了一个demo可以显示出来,然后就开始不停查找webview设置的属性是不是哪里不对,然后并没有结果。这时候就只能查看源码了。
在4.2的源码上,通过surfaceview和mediacontrol生成两个view,放到framelayout中,通过webview的回调返回给webview,webview收到这个回调,将这个view加入到布局中实现布局。
现在的问题是MediaControl这个view不见了,接着查看MediaControl这个类生成view的地方,因为我们的webview是通过插件的布局文件生成的,WebView的Context是我们自定义的DLPluginContext,所以这里要生成view必须通过inflate.cloneInContext(),导致这个view没有生成成功。
所以我们可以通过new WebView(Context context)生成WebView,这个context传入的是我们当前的activity,然后加入到布局中就可以啦。
做了插件化代理方式、预注册方式,遇到好一些问题都是关于Context,宿主的Context,插件的Context,一定要用正确。
当时没有注意,下载需要5积分,我也没有找到地方哪里可以改,有需要的请留言。
这篇流水账就到这里结束啦,欢迎大家吐槽。