Android插件开发 —— 通过预注册方式打开activity(记录我踩过的坑)

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、代理方式:参见“木质的旋律”大大的博客http://blog.csdn.net/h28496/article/details/50414873
2、预注册的方式:参见“木质的旋律”大大的博客http://blog.csdn.net/H28496/article/details/49966503

首先我们的宿主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
图1
图2
图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设置的属性是不是哪里不对,然后并没有结果。这时候就只能查看源码了。
在这里,我给大家安利一个很好的看源码的网站,各个不同的版本源码都有:http://androidxref.com/
在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,一定要用正确。

最后,关于预注册的代码下载见:http://download.csdn.net/download/shuishuixiaoping/9929970
当时没有注意,下载需要5积分,我也没有找到地方哪里可以改,有需要的请留言。

这篇流水账就到这里结束啦,欢迎大家吐槽。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值