Style中的輕量級插件化方案

wr0416 7年前發布 | 21K 次閱讀 安卓熱修復 Android開發 移動開發

閱讀之前

  • 建議 下載使用 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

 

 本文由用戶 wr0416 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!