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

来源:互联网 发布:怎样退货给淘宝卖家 编辑:程序博客网 时间:2024/06/06 13:58

前言:

一直以来就对插件化这技术推崇已久,在去年也写过两篇关于插件化基础的文章:
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下载


阅读全文
0 0
原创粉丝点击