Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 拆輪子之動態加載DynamicLoadApk

拆輪子之動態加載DynamicLoadApk

編輯:關於Android編程

動態加載是什麼

應用在運行的時候通過加載一些本地不存在的可執行文件實現一些特定的功能,Android中動態加載的核心思想是動態調用外部的Dex文件,極端的情況下,Android APK自身帶有的Dex文件只是一個程序的入口(或者說空殼),所有的功能都通過從服務器下載最新的Dex文件完成。

動態加載技術的運用

1、可以縮小apk體積,比如一個app的一些不常用但又不得不要的模塊可以采用放在插件中,通過下載插件加載進來獲取功能,以此來減少apk體積
2、從項目管理上來看,分割插件模塊的方式做到了 項目級別的代碼分離,大大降低模塊之間的耦合度,同一個項目能夠分割出不同模塊在多個開發團隊之間 並行開發,如果出現BUG也容易定位問題
3,、可以緊急修復一些bug,而不必重新發包讓用戶進行下載安裝如此繁瑣的過程

動態加載技術框架

360DroidPlugin
DynamicLoadApk
DynamicApk
Nuwa
等….以上技術都開源且具有一定知名度,今天挑一個下手分析下它是如何進行動態加載的

DynamicLoadApk加載原理

生硬地講解原理有點難理解,做了個小demo,宿主apk安裝在手機上,然後調起在sd卡的plugin.apk,啟動完插件後,可以在插件裡啟動Activity,Service,就跟操作一個app一樣。

解析插件Apk

先看看第一部宿主apk如何調用起插件apk的

if(!file.exists()){
    Toast.makeText(MainActivity.this, "插件apk不存在,請在sd卡目錄放plugin.apk作為插件", Toast.LENGTH_SHORT).show();
    return;
}
DLPluginManager pluginManager = DLPluginManager.getInstance(BaseApplicaiton.getInstance());
DLPluginPackage dlPluginPackage = pluginManager.loadApk(pluginApkPath);
pluginManager.startPluginActivity(this, new DLIntent(dlPluginPackage.packageName, dlPluginPackage.defaultActivity));

上面代碼最重要部分就是loadApk方法,傳入了我們插件apk的文件路徑。
最後其實調用的是

public DLPluginPackage loadApk(final String dexPath, boolean hasSoLib) {
    mFrom = DLConstants.FROM_EXTERNAL;

     PackageInfo packageInfo = mContext.getPackageManager().getPackageArchiveInfo(dexPath,
     PackageManager.GET_ACTIVITIES | PackageManager.GET_SERVICES);
     if (packageInfo == null) {
        return null;
     }

    DLPluginPackage pluginPackage = preparePluginEnv(packageInfo, dexPath);
    if (hasSoLib) {
       copySoLib(dexPath);
    }

    return pluginPackage;
}

上面的方法我們可以看到返回一個DLPluginPackage,構造它的方法是第十行的preparePluginEnv

private DLPluginPackage preparePluginEnv(PackageInfo packageInfo, String dexPath) {

    DLPluginPackage pluginPackage = mPackagesHolder.get(packageInfo.packageName);
    if (pluginPackage != null) {
       return pluginPackage;
    }
    DexClassLoader dexClassLoader = createDexClassLoader(dexPath);
    AssetManager assetManager = createAssetManager(dexPath);
    Resources resources = createResources(assetManager);
    // create pluginPackage
    pluginPackage = new DLPluginPackage(dexClassLoader, resources, packageInfo);
    mPackagesHolder.put(packageInfo.packageName, pluginPackage);
    return pluginPackage;
}
private DexClassLoader createDexClassLoader(String dexPath) {
    File dexOutputDir = mContext.getDir("dex", Context.MODE_PRIVATE);
    dexOutputPath = dexOutputDir.getAbsolutePath();
    DexClassLoader loader = new DexClassLoader(dexPath, dexOutputPath, mNativeLibDir,      mContext.getClassLoader());
    return loader;
}

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;
 }

}
private Resources createResources(AssetManager assetManager) {
    Resources superRes = mContext.getResources();
    Resources resources = new Resources(assetManager, superRes.getDisplayMetrics(), superRes.getConfiguration());
    return resources;
}

上面代碼很明確,我們知道createDexClassLoader就是直接加載我們的插件apk,Android項目中,所有Java代碼都會被編譯成dex文件,Android應用運行時,就是通過執行dex文件裡的業務代碼邏輯來工作的。
因此我們通過DexClassLoader來獲取插件apk裡面的業務邏輯包括Activity,Service之類,也可以說是插件裡java方面的代碼,但開發app的都知道,我們app裡不只有java代碼,還有xml那些資源文件,這些又
要如何獲取呢,首先構造一個AssetManager,利用反射把我們的插件apk路徑設置進去,並根據這個AssetManager得到Resource。這樣我們就獲得了最重要的三個東西,dexClasssLoader,為後面加載去插件
類做准備,resource,為後面獲取插件裡的資源信息做准備,同時還有一個packageInfo,這個是android提供的解析類,可以獲取插件apk中的啟動Activity。
當然了,現在的apk還包含有so文件,我們的插件apk也會有so文件,這個文件又該如何處理,這裡不貼代碼,直接講下原理,就是解析插件apk包得到其中的so文件復制到宿主apk的/data/data/…..目錄下,保證
可以加載到該so文件

運行插件Apk

經過前面的步驟,我們了解了如何解析插件apk得到相應的信息,資源。現在看看如何利用這些東西運行插件,首先看看整個框架的總體設計
這裡寫圖片描述
最底下黃色那層的類都是插件裡Activity,Service的基類,運行插件簡單說就是跑這些基類的子類,但開發過android的程序的都知道,Activity,Service有自身的生命周期,插件裡的Activity,Service的
生命周期要怎麼控制呢。DL框架用了一套比較巧妙的方式,代理的方式。每次啟動插件Activity或者Service本質上啟動的是宿主裡面的DLProxyActivity(上圖綠色那層類),因為在宿主程序裡啟動的Activity
或者Service,它們的生命周期將會由系統控制,我們只要在對應的代理類的生命周期函數中調用對應的插件裡的 生命周期方法。
這裡寫圖片描述
具體看看代碼如何實現,啟動代理的Activity,分別可以在DLPluginManager裡調用startPluginActivityForResult(一般宿主使用),還有就是在DLPluginActivity裡使用startPluginActivity(在插件裡使用),它們的調用最後都是殊途同歸。我們調後者來研究下
DLIntent intent = new DLIntent(getPackageName(), SecondActivity.class);
startPluginActivity(intent);
上面是調用Activity方法,再來往下看代碼<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwcmUgY2xhc3M9"brush:java;"> public int startPluginActivity(DLIntent dlIntent) { return startPluginActivityForResult(dlIntent, -1); } /** * @param dlIntent * @return may be {@link #START_RESULT_SUCCESS}, * {@link #START_RESULT_NO_PKG}, {@link #START_RESULT_NO_CLASS}, * {@link #START_RESULT_TYPE_ERROR} */ public int startPluginActivityForResult(DLIntent dlIntent, int requestCode) { if (mFrom == DLConstants.FROM_EXTERNAL) { if (dlIntent.getPluginPackage() == null) { dlIntent.setPluginPackage(mPluginPackage.packageName); } } return mPluginManager.startPluginActivityForResult(that, dlIntent, requestCode); }

上面方法有兩個參數,一個是mFrom,一個是that,先說它們代表的意思,後面再來分析它們怎麼來的,mFrom代表當前這個Acitivity是用來當做插件啟動(FROM_EXTERNAL)還是宿主啟動(FROM_INTERNAL),
一般情況下都是當做插件啟動。如果是當做插件啟動that就代表代理Activity,宿主啟動that代表自身。
繼續看mPluginManager.startPluginActivityForResult

public int startPluginActivityForResult(Context context, DLIntent dlIntent, int requestCode) {
    if (mFrom == DLConstants.FROM_INTERNAL) {
        dlIntent.setClassName(context, dlIntent.getPluginClass());
        performStartActivityForResult(context, dlIntent, requestCode);
        return DLPluginManager.START_RESULT_SUCCESS;
 }

    String packageName = dlIntent.getPluginPackage();
    if (TextUtils.isEmpty(packageName)) {
        throw new NullPointerException("disallow null packageName.");
    }

    DLPluginPackage pluginPackage = mPackagesHolder.get(packageName);
    if (pluginPackage == null) {
        return START_RESULT_NO_PKG;
    }

    final String className = getPluginActivityFullPath(dlIntent, pluginPackage);
    Class clazz = loadPluginClass(pluginPackage.classLoader, className);
    if (clazz == null) {
        return START_RESULT_NO_CLASS;
    }

    // get the proxy activity class, the proxy activity will launch the
    // plugin activity.
    Class activityClass = getProxyActivityClass(clazz);
    if (activityClass == null) {
       return START_RESULT_TYPE_ERROR;
    }

    // put extra data
    dlIntent.putExtra(DLConstants.EXTRA_CLASS, className);
    dlIntent.putExtra(DLConstants.EXTRA_PACKAGE, packageName);
    dlIntent.setClass(mContext, activityClass);
    performStartActivityForResult(context, dlIntent, requestCode);
    return START_RESULT_SUCCESS;
}

先進行一些判斷,然後獲取要啟動Activity的名稱(包名+類名),然後利用反射獲取插件類實例activityClass

private Class loadPluginClass(ClassLoader classLoader, String className) {
    Class clazz = null;
    try {
       clazz = Class.forName(className, true, classLoader);
    } catch (ClassNotFoundException e) {
        e.printStackTrace();
    }

    return clazz;
}

根據插件類實例獲取它對應在宿主裡對應的代理類

private Class getProxyActivityClass(Class clazz) {
    Class activityClass = null;
    if (DLBasePluginActivity.class.isAssignableFrom(clazz)) {
        activityClass = DLProxyActivity.class;
    } else if (DLBasePluginFragmentActivity.class.isAssignableFrom(clazz)) {
        activityClass = DLProxyFragmentActivity.class;
    } else if (Activity.class.isAssignableFrom(clazz)){
        activityClass = Activity.class;
    }

    return activityClass;
}

可以看到就跟設計圖裡的一樣
**DLBasePluginActivity -> DLProxyActivity
DLBasePluginFragmentActivity -> DLProxyFragmentActivity**
這裡基本要完成偷天換主…表明上啟動的是一個插件類,其實要啟動一個宿主代理類

dlIntent.putExtra(DLConstants.EXTRA_CLASS, className);
dlIntent.putExtra(DLConstants.EXTRA_PACKAGE, packageName);
dlIntent.setClass(mContext, activityClass);
performStartActivityForResult(context, dlIntent, requestCode);

這邊把插件包名,要啟動的插件類名當做參數設置到Intent,而真正設置給intent啟動的是宿主的代理class

private void performStartActivityForResult(Context context, DLIntent dlIntent, int requestCode) {
    Log.d(TAG, "launch " + dlIntent.getPluginClass());
    if (context instanceof Activity) {
        ((Activity) context).startActivityForResult(dlIntent, requestCode);
    } else {
        context.startActivity(dlIntent);
    }
}

這個代碼是不是就很熟悉,就跟我們平時開發app啟動activity一樣,現在代理Activity啟動了…

通過代理類運行插件類

我們這裡拿一個代理類來分析DLProxyActivity,經過前面的分析,它被啟動了,當然就執行到我們很熟悉的onCreate了,看看代理類怎麼實行偷天換日的..

protected DLPlugin mRemoteActivity;
private DLProxyImpl impl = new DLProxyImpl(this);

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    impl.onCreate(getIntent());
}

在onCreate裡讓DLProxyImpl去執行onCreate

public void onCreate(Intent intent) {

    // set the extra's class loader
    intent.setExtrasClassLoader(DLConfigs.sPluginClassloader);

    mPackageName = intent.getStringExtra(DLConstants.EXTRA_PACKAGE);
    mClass = intent.getStringExtra(DLConstants.EXTRA_CLASS);
    Log.d(TAG, "mClass=" + mClass + " mPackageName=" + mPackageName);

    mPluginManager = DLPluginManager.getInstance(mProxyActivity);
    mPluginPackage = mPluginManager.getPackage(mPackageName);
    mAssetManager = mPluginPackage.assetManager;
    mResources = mPluginPackage.resources;

    initializeActivityInfo();
    handleActivityInfo();
    launchTargetActivity();
}

在initializeActivityInfo之前是獲取intent傳遞過來的插件包名,插件類,並根據它們去獲取插件信息類PluginPackage,裡面存放插件的resource,assetmanager,這裡取出來為後面獲取對應資源做准備
initializaActivityInfo和handleActivityInfo主要是進行一些主題處理,這裡不做分析,重點看launchTargetActivity

protected void launchTargetActivity() {
    try {
        Class localClass = getClassLoader().loadClass(mClass);
        Constructor localConstructor = localClass.getConstructor(new Class[] {});
        Object instance = localConstructor.newInstance(new Object[] {});
        mPluginActivity = (DLPlugin) instance;
        ((DLAttachable) mProxyActivity).attach(mPluginActivity, mPluginManager);
        Log.d(TAG, "instance = " + instance);
        // attach the proxy activity and plugin package to the mPluginActivity
        mPluginActivity.attach(mProxyActivity, mPluginPackage);

        Bundle bundle = new Bundle();
        bundle.putInt(DLConstants.FROM, DLConstants.FROM_EXTERNAL);
        mPluginActivity.onCreate(bundle);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

前面幾行先根據mClass初始化插件類,並賦值到mPluginActivity,然後是兩個attach方法
第一個是代理類的attach方法
public void attach(DLPlugin remoteActivity, DLPluginManager pluginManager) {
mRemoteActivity = remoteActivity;
}
第二個是插件類的attach

@Override
public void attach(Activity proxyActivity, DLPluginPackage pluginPackage) {
    Log.d(TAG, "attach: proxyActivity= " + proxyActivity);
    mProxyActivity = (Activity) proxyActivity;
    that = mProxyActivity;
    mPluginPackage = pluginPackage;
}

這裡看到插件類裡的that被賦值了代理類,所以我們在編寫插件的代碼的時候this的功能將被廢棄,不能使用this,必須使用that,上面的分析大家也看到,真正各種操作都是要交給代理類執行,
因此比如我們要setContentView,finish之類的方法都應該用的是代理的setContentView,finish,所以必須用that.XXXX去做處理。
做完以上處理後,就是直接

Bundle bundle = new Bundle();
bundle.putInt(DLConstants.FROM, DLConstants.FROM_EXTERNAL);
mPluginActivity.onCreate(bundle);
 ```
告訴插件類你是FROM_EXTERNAL的,然後觸發它的onCreate,這麼一路下來,代理onCreate方法完成了對插件onCreate的偷天換日...

分析完onCreate,onResume什麼的就類似了,當代理onResume被系統觸發

@Override
protected void onResume() {
mRemoteActivity.onResume();
super.onResume();
}
“`
其實它什麼都不做就是觸發對應插件類的onResume。

還有service的過程也一樣,這裡就不做分析了,和activity類似的流程。

DynamicLoadApk的不足

1、經過對DynamicLoadApk的分析我們知道它很大程度是通過代理的方式進行插件化,這也意味著一些靜態注冊由系統啟動的BroadcastReceiver,ContentProvider無法支持
2、插件需要用that不能用this,這也的寫法有點小別扭
3、 不支持自定義主題,不支持系統透明主題
4、插件和宿主資源 id 可能重復的問題沒有解決,需要修改 aapt 中資源 id 的生成規則

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved