Style中的轻量级插件化方案

阅读之前

  • 建议下载使用Style动态壁纸应用
  • 文章后面会给出相应引用的链接
  • 如需评论请翻墙后刷新页面

文章主要讲以下几个方面:

  • 插件化背景及优缺点
  • Style中的轻量级插件
  • Style壁纸插件开发者SDK介绍
  • 构建第一个Style壁纸组件
  • 其他注意问题

Android插件化背景

Android中的插件化,旨在无需安装、修改的情况下运行APK。例如某应用包含非常多的功能,除了核心功能之外,其他功能都与主要业务比较独立,那么其他功能模块就可以考虑用插件方式开发。

目前国内比较大的几款应用都有类似的场景。各大厂也有相应的插件框架,例如BAT、滴滴的VirtualAPK、360的DroidPlugin等等。而且VirtualAPK和DroidPlugin是开源框架,大家有兴趣可以深入学习研究。

从我自身对插件化的接触,对它的优缺点做了一些总结:

优点:

  • 减少宿主包大小,插件模块按需下载
  • 实现多团队协作开发,由独立团队对插件进行开发
  • 插件动态更新,插件包可以随时用户无感知的进行更新

缺点:

  • 插件框架开发难度较大,插件框架开发需要深入了解Android的系统源码,并需要对系统级别方法进行大量hook,处理各种高难度问题
  • 插件的内存不易管理,如果插件和宿主存在同一进程中,那么宿主的内存使用是宿主+插件两者的内存使用之和,甚至导致宿主OOM,通常需要将插件放在独立进程中
  • 对插件包的大小、插件的质量要求比较严格。插件包太大宿主在首次加载时会消耗太多流量下载。插件如果运行出现异常,可能会导致宿主崩溃,影响用户体验
  • 插件很难做到完全不修改,插件框架也很难兼容系统的所有组件

上述问题虽然难度较大,但并非不能解决,相信各大公司都有比较完善但插件框架。小公司如果没有大牛,在使用的时候也须谨慎。

Style中的轻量级插件

Style中包含三类壁纸:特效壁纸、Style艺术图片、自定义照片。其中Style艺术图片和自定义照片都是将一张图片渲染成壁纸,因此两者的渲染逻辑是一样的。而特效壁纸每一个都有不一样的效果,渲染逻辑代码都不一样。

考虑到这一点,Style将特效壁纸做成插件的形式。有新的壁纸增加时,Style能及时更新并动态加载新的壁纸。另外,这种插件不需要是一个完整的APK,因为Style只会加载里面的WallpaperService类以使用它的渲染逻辑。

因此Style中的插件就不需要太完整,这样能大大简化插件框架的开发,简化插件的开发。Style中将这种不完整的插件称之为壁纸组件,下面我会用“组件”这个词来表示Style中的插件。

engine模块

“engine”模块包含了运行组件的所有代码。Manifest中注册的WallpaperService类在“presentation”模块中,依赖了“engine”模块。代码如下:

public class StyleWallpaperService extends WallpaperService {
    private ProxyProvider proxyProvider = new ProxyProvider();
    private WallpaperService proxy;
    @Override
    public void onCreate() {
        super.onCreate();
        proxy = proxyProvider.provideProxy(this);
        if (proxy != null) {
            proxy.onCreate();
        }
    }
    @Override
    public void onDestroy() {
        super.onDestroy();
        if (proxy != null) {
            proxy.onDestroy();
        }
    }
    @Override
    public Engine onCreateEngine() {
        if (proxy != null) {
            return proxy.onCreateEngine();
        } else {
            return new Engine();
        }
    }
}

可以看出系统的WallpaperService简单的将所有逻辑交给我们的代理Service,onCreateEngine()方法中也是由代理返回Engine对象。

我们一共有两个代理类:WallpaperServiceProxyGLWallpaperServiceProxy,他们提供了不同的渲染支持。以下将这两个代理类简称为Proxy

public class WallpaperServiceProxy extends WallpaperService {
    public WallpaperServiceProxy(Context host) {
        attachBaseContext(host);
    }
    @Override
    public Engine onCreateEngine() {
        return null;
    }
    public class ActiveEngine extends Engine {
    }
}
public class GLWallpaperServiceProxy extends GLWallpaperService {
    public GLWallpaperServiceProxy(Context host) {
        attachBaseContext(host);
    }
    public class GLActiveEngine extends GLEngine {
    }
}

在Proxy中,我们会有一个带有Context对象参数的构造方法,并在构造方法中利用attachBaseContext(host)指定了Proxy对象的Context。但是这个Context对象是一个特殊的Context,组件会通过它来获取ClassLoaderResource,来加载组件中的类和资源。

那么下面我们来看看这个Context到底有什么特殊:

public class ComponentContext extends ContextWrapper {
    private String componentPath;
    private Resources mResources;
    private ClassLoader mClassLoader;
    public ComponentContext(Context base, String componentPath) {
        super(base.getApplicationContext());
        this.componentPath = componentPath;
    }
    @Override
    public Resources getResources() {
        if (mResources == null) {
            mResources = ResourcesManager.createResources(getBaseContext(), componentPath);
        }
        return mResources;
    }
    @Override
    public ClassLoader getClassLoader() {
        return getClassLoader(componentPath);
    }
    private ClassLoader getClassLoader(String componentFilePath) {
        if (mClassLoader == null) {
            mClassLoader = new DexClassLoader(componentFilePath, getCacheDir().getAbsolutePath(),
                    null, getBaseContext().getClassLoader());
        }
        return mClassLoader;
    }
    @Override
    public AssetManager getAssets() {
        return getResources().getAssets();
    }
    @Override
    public Context getApplicationContext() {
        return this;
    }
}

首先它是ContextWrapper的子类,并且有一个构造方法,构造方法第一个参数是宿主的Application Context对象,第二个参数是组件包的存放路径。

然后它重写了四个常用的方法:getResources()getClassLoader()getAssets()getApplicationContext(),前三方法返回了跟组件相关的类加载器、资源、和AssetsManager,最后一个方法是为了兼容组件中getApplicationContext()的使用。

除了这四个方法外,其他的Context中的方法均有使用宿主现有的方法。轻量级可以说就是这个意思,意味着宿主只关心组件中的类和资源,不关心里面的系统组件和其他东西。

我们再来看看IProvider这个接口。组件中通过实现它来返回组件中的Proxy实现

public interface IProvider {
    WallpaperService provideProxy(Context host);
}

那么我们在宿主中是如何获取到它的实现呢?

public class ProxyApi {
    private static IProvider getProvider(ComponentContext context, String providerName)
            throws Exception {
        synchronized (ProxyApi.class) {
            IProvider provider;
            Class providerClazz = context.getClassLoader().loadClass(providerName);
            if (providerClazz != null) {
                provider = (IProvider) providerClazz.newInstance();
                return provider;
            } else {
                throw new IllegalStateException("Load Provider error.");
            }
        }
    }
    public static WallpaperService getProxy(Context context,
                                            String componentPath, String providerName) {
        try {
            ComponentContext componentContext = new ComponentContext(context, componentPath);
            IProvider provider = getProvider(componentContext, providerName);
            return provider.provideProxy(componentContext);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

ProxyApi类的getProvider()方法中,通过先前的ComponentContext对象获取到组件的ClassLoader,再通过IProvider实现的类名来加载其实现。在getProxy()方法中构造了ComponentContext实例,并通过IProvider的实现获取到组件中的Proxy对象。

将上面的逻辑串联起来,我们发现StyleWallpaperService可以和组件中的Proxy对象直接交互了。那么Proxy对象就来完成壁纸的渲染工作。就是说壁纸的渲染工作我们交给了组件来处理。

架构

图中粉色部分是宿主,也就是Style。紫色部分是组件,也就是壁纸具体的实现。插件化方案将它们解耦,让壁纸的实现实现了动态部署、热更新、热修复。

Style壁纸插件开发者SDK介绍

插件化的一个好处是可以跨团队协作,由其他团队进行插件开发。因此Style的特效壁纸便开放给开发者,任何人都可以参与开发。

Style提供了一套简易的SDK,供开发者以Style组件的规范进行壁纸开发。SDK可以从Github上找到。它包含三个模块:sdk、壳、具体的实现。sdk和壳模块不需要任何的修改,开发者主要在实现模块进行开发。三者的依赖关系如下: 依赖关系

实现模块编译时依赖sdk模块来使用IProvder和Proxy类,但必须是使用“Provided”构建,防止将sdk的代码打入组件包中。它们都是library模块。

壳模块没有代码、资源,是一个简单的application模块。目的是将实现模块编译进去,以生成APK包。

相对于宿主运行环境(“engine”模块),sdk模块提供的代码则精简很多,它只是提供了编译环境,不需要任何实现。 sdk

实现模块也只有两个东西,IProvider的实现类、Proxy的实现类。代码量视渲染逻辑的复杂度有所区别。 实现

可能你会有个疑问,就是构建好壁纸组件之后我们如何对它进行测试?直接用Style应用吗?

在sdk工程根目录中,我提供了一个测试应用:sdk_test.apk。它包含了Style运行壁纸组件的完整环境。简单的说,如果它能加载壁纸组件并成功运行,那么Style也能。完整的测试步骤可以参看说明

构建第一个Style壁纸组件

好了,花了大篇幅讲述Style中组件的原理和开发者sdk。现在我们来尝试使用sdk构建一款Style壁纸组件。

上一篇文章我讲了Android如何创建动态壁纸。里面的第一个例子显示了一些圆点。完整的代码可以看这里demo1 下面我就用这个例子来说明如何利用sdk来构建壁纸组件。

1、我们新建一个工程,Activity什么的都不需要。 2、在新工程中新建sdk(library)模块,并将sdk的代码放进去。注意包名必须是com.yalin.style.enginenet.rbgrn.android.glwallpaperservice,后面我会将它放到Maven仓库中,一句代码就可以搞定了。 3、新建实现模块point_wallpaper(library),并在它的build.gradle中添加依赖provided project(':sdk')。 4、在point_wallpaper模块中创建自己的WallpaperService实现类,PointWallpaperServiceProxy继承自WallpaperServiceProxy

public class PointWallpaperServiceProxy extends WallpaperServiceProxy {
    public PointWallpaperServiceProxy(Context host) {
        super(host);
    }
    @Override
    public Engine onCreateEngine() {
        return new MyWallpaperEngine();
    }
    private class MyWallpaperEngine extends ActiveEngine {
        private final Handler handler = new Handler();
        private final Runnable drawRunner = new Runnable() {
            @Override
            public void run() {
                draw();
            }
        };
        private List<MyPoint> circles;
        private Paint paint = new Paint();
        private int width;
        int height;
        private boolean visible = true;
        private int maxNumber;
        private boolean touchEnabled;
        public MyWallpaperEngine() {
            maxNumber = 4;
            touchEnabled = true;
            circles = new ArrayList<>();
            paint.setAntiAlias(true);
            paint.setColor(Color.WHITE);
            paint.setStyle(Paint.Style.STROKE);
            paint.setStrokeJoin(Paint.Join.ROUND);
            paint.setStrokeWidth(10f);
            handler.post(drawRunner);
        }
        @Override
        public void onVisibilityChanged(boolean visible) {
            this.visible = visible;
            if (visible) {
                handler.post(drawRunner);
            } else {
                handler.removeCallbacks(drawRunner);
            }
        }
        @Override
        public void onSurfaceDestroyed(SurfaceHolder holder) {
            super.onSurfaceDestroyed(holder);
            this.visible = false;
            handler.removeCallbacks(drawRunner);
        }
        @Override
        public void onSurfaceChanged(SurfaceHolder holder, int format,
                                     int width, int height) {
            this.width = width;
            this.height = height;
            super.onSurfaceChanged(holder, format, width, height);
        }
        @Override
        public void onTouchEvent(MotionEvent event) {
            if (touchEnabled) {
                float x = event.getX();
                float y = event.getY();
                SurfaceHolder holder = getSurfaceHolder();
                Canvas canvas = null;
                try {
                    canvas = holder.lockCanvas();
                    if (canvas != null) {
                        canvas.drawColor(Color.BLACK);
                        circles.clear();
                        circles.add(new MyPoint(
                                String.valueOf(circles.size() + 1), (int) x, (int) y));
                        drawCircles(canvas, circles);
                    }
                } finally {
                    if (canvas != null)
                        holder.unlockCanvasAndPost(canvas);
                }
                super.onTouchEvent(event);
            }
        }
        private void draw() {
            SurfaceHolder holder = getSurfaceHolder();
            Canvas canvas = null;
            try {
                canvas = holder.lockCanvas();
                if (canvas != null) {
                    if (circles.size() >= maxNumber) {
                        circles.clear();
                    }
                    int x = (int) (width * Math.random());
                    int y = (int) (height * Math.random());
                    circles.add(new MyPoint(String.valueOf(circles.size() + 1),
                            x, y));
                    drawCircles(canvas, circles);
                }
            } finally {
                if (canvas != null)
                    holder.unlockCanvasAndPost(canvas);
            }
            handler.removeCallbacks(drawRunner);
            if (visible) {
                handler.postDelayed(drawRunner, 1000);
            }
        }
        // Surface view requires that all elements are drawn completely
        private void drawCircles(Canvas canvas, List<MyPoint> circles) {
            canvas.drawColor(Color.BLACK);
            for (MyPoint point : circles) {
                canvas.drawCircle(point.x, point.y, 20.0f, paint);
            }
        }
    }
}

这里是用了MyPoint类,代码如下:

public class MyPoint {
    String text;
    int x;
    int y;
    public MyPoint(String text, int x, int y) {
        this.text = text;
        this.x = x;
        this.y = y;
    }
}

5、实现IProvider并返回第四步的PointWallpaperServiceProxy实例

public class ProviderImpl implements IProvider {
    @Override
    public WallpaperService provideProxy(Context host) {
        return new PointWallpaperServiceProxy(host);
    }
}

6、将新建工程时的app模块当作壳模块,引用point_wallpaper模块,compile project(':point_wallpaper')。 7、运行_./gradlew assemble_,生成apk文件,有没有签名没有关系,并将它放到/sdcard/style/目录下,假设名称叫point.apk。 8、安装测试应用(sdk中的sdk_test.apk) 9、创建配置文件config.json,加入下面json。也将它放到/sdcard/style/目录下。point.apk是/sdcard/style/中组件的文件名。“provider_name”是IProvider实现类的完整类名。

{
  "component_name": "point.apk",
  "provider_name": "com.yalin.wallpaper.point.ProviderImpl"
}

10、运行测试应用,点击设置壁纸按钮。出现下面的界面 demo2

简单的几步,第一个组件应用建好了,并能在宿主中运行。你也可以将这些方法运用到你的项目中去。你可以对比组件中的渲染逻辑和原来demo中的渲染逻辑,他们完全是一样的。也就印证了上面说的简化组件开发(因为可以直接把现成的移植过来)。

其他注意问题

  • 组件混淆 -keep class * implements com.yalin.style.engine.IProvider
  • 打包时尽可能删掉其他不需要的依赖(例如appcompat-v7),以减小组件包大小。
  • 组件包中可以包含资源和Assets,但是现在不支持运行.so。
  • 运行测试应用时记得为它开启读取外存权限。

总结

也许我们潜移默化的认为,只有大的项目才有机会用到插件化,毕竟小公司业务相对不复杂、也不一定有那么多人去维护插件框架。但是通过这次实验,我们完全可以将一些功能做成轻量级插件,以实现动态的更新发布、分团队开发、解耦。而且宿主中插件相关的代码量非常少,几百来行,易于维护。那么,何不在你的项目中试试呢。

插件相关开源项目

VirtualApk(滴滴)

DroidPlugin(360)