Style中的輕量級插件化方案
閱讀之前
- 建議 下載使用 Style動態壁紙應用
- 文章后面會給出相應引用的鏈接
- 如需評論請F墻后刷新頁面
文章主要講以下幾個方面:
- 插件化背景及優缺點
- 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”模塊包含了運行組件的所有代碼。 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();
}
}
}</code></pre>
可以看出系統的 WallpaperService 簡單的將所有邏輯交給我們的代理Service, onCreateEngine() 方法中也是由代理返回 Engine 對象。
我們一共有兩個代理類: WallpaperServiceProxy 和 GLWallpaperServiceProxy ,他們提供了不同的渲染支持。以下將這兩個代理類簡稱為Proxy
public class WallpaperServiceProxy extends WallpaperService {
public WallpaperServiceProxy(Context host) {
attachBaseContext(host);
}
@Override
public Engine onCreateEngine() {
return null;
}
public class ActiveEngine extends Engine {
}
}</code></pre>
public class GLWallpaperServiceProxy extends GLWallpaperService {
public GLWallpaperServiceProxy(Context host) {
attachBaseContext(host);
}
public class GLActiveEngine extends GLEngine {
}
}</code></pre>
在Proxy中,我們會有一個帶有 Context 對象參數的構造方法,并在構造方法中利用 attachBaseContext(host) 指定了Proxy對象的Context。但是這個 Context 對象是一個特殊的 Context ,組件會通過它來獲取 ClassLoader 和 Resource ,來加載組件中的類和資源。
那么下面我們來看看這個 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;
}
}</code></pre>
首先它是 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;
}
}</code></pre>
在 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模塊提供的代碼則精簡很多,它只是提供了編譯環境,不需要任何實現。 
實現模塊也只有兩個東西, IProvider 的實現類、Proxy的實現類。代碼量視渲染邏輯的復雜度有所區別。 
可能你會有個疑問,就是構建好壁紙組件之后我們如何對它進行測試?直接用Style應用嗎?
在sdk工程根目錄中,我提供了一個測試應用:sdk_test.apk。它包含了Style運行壁紙組件的完整環境。簡單的說,如果它能加載壁紙組件并成功運行,那么Style也能。完整的測試步驟可以參看 說明
構建第一個Style壁紙組件
好了,花了大篇幅講述Style中組件的原理和開發者sdk。現在我們來嘗試使用sdk構建一款Style壁紙組件。
上一篇文章 我講了Android如何創建動態壁紙。里面的第一個例子顯示了一些圓點。完整的代碼可以看 這里 。
下面我就用這個例子來說明如何利用sdk來構建壁紙組件。
1、我們新建一個工程,Activity什么的都不需要。 2、在新工程中新建sdk(library)模塊,并將sdk的代碼放進去。注意包名必須是 com.yalin.style.engine 和 net.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);
}
}
}
}</code></pre>
這里是用了 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;
}
}</code></pre>
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、運行測試應用,點擊設置壁紙按鈕。出現下面的界面 
簡單的幾步,第一個組件應用建好了,并能在宿主中運行。你也可以將這些方法運用到你的項目中去。你可以對比組件中的渲染邏輯和原來demo中的渲染邏輯,他們完全是一樣的。也就印證了上面說的簡化組件開發(因為可以直接把現成的移植過來)。
其他注意問題
- 組件混淆 -keep class * implements com.yalin.style.engine.IProvider
- 打包時盡可能刪掉其他不需要的依賴(例如appcompat-v7),以減小組件包大小。
- 組件包中可以包含資源和Assets,但是現在不支持運行.so。
- 運行測試應用時記得為它開啟讀取外存權限。
總結
也許我們潛移默化的認為,只有大的項目才有機會用到插件化,畢竟小公司業務相對不復雜、也不一定有那么多人去維護插件框架。但是通過這次實驗,我們完全可以將一些功能做成輕量級插件,以實現動態的更新發布、分團隊開發、解耦。而且宿主中插件相關的代碼量非常少,幾百來行,易于維護。那么,何不在你的項目中試試呢。
來自:http://www.jcodecraeer.com/a/anzhuokaifa/2017/0807/8350.html