Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android插件化探索(三)免安裝運行Activity(上)

Android插件化探索(三)免安裝運行Activity(上)

編輯:關於Android編程

前情提要

在上一篇中有一個細節沒有提到,那就是getResourcesForApplication和AssetManager的區別。

getResourcesForApplication

getResourcesForApplication(String packageName),很顯然需要傳入一個包名,換言之,這個插件必須已經被安裝在系統內,然後才能通過包名來獲取資源。你可能會想,不安裝照樣可以獲取包名啊。的確,通過pm.getPackageArchiveInfo()可以獲取安裝包信息。但是,這些包都是沒有在PMS中注冊的。如果仍然這樣獲取,會提示如下錯誤。

android.content.pm.PackageManager$NameNotFoundException: com.maplejaw.hotplugin

現在我們就從源碼角度來分析getResourcesForApplication。源碼在ApplicationPackageManager中,如下:

    @Override
    public Resources getResourcesForApplication(String appPackageName)
            throws NameNotFoundException {
        return getResourcesForApplication(
            getApplicationInfo(appPackageName, sDefaultFlags));
    }

可以看出內部調用了重載方法。getApplicationInfo返回的是ApplicationInfo對象。

    @Override
    public Resources getResourcesForApplication(@NonNull ApplicationInfo app){

        //...
        //省略了部分源碼
        final Resources r = mContext.mMainThread.getTopLevelResources(
                sameUid ? app.sourceDir : app.publicSourceDir,
                sameUid ? app.splitSourceDirs : app.splitPublicSourceDirs,
                app.resourceDirs, app.sharedLibraryFiles, Display.DEFAULT_DISPLAY,
                null, mContext.mPackageInfo);
        if (r != null) {
            return r;
        }

    }

最終走的仍舊是ActivityThread的getTopLevelResources,ActivityThread裡面的相關源碼我就不分析了,跟上一篇是一樣的也是調用ResourcesManager中的getTopLevelResources,這裡不做贅述。

現在我們主要來看看getApplicationInfo裡面做了什麼?

  @Override
public ApplicationInfo getApplicationInfo(String packageName, int flags) throws NameNotFoundException {
      ApplicationInfo ai =   mPM.getApplicationInfo(packageName, flags, mContext.getUserId());
        //...
        //省略了部分源碼
        throw new NameNotFoundException(packageName);
    }

mPM的初始化源碼如下,可以看出是一個PMS(PackageManagerService)對象。

    public static IPackageManager getPackageManager() {
        if (sPackageManager != null) {
            return sPackageManager;
        }
        IBinder b = ServiceManager.getService("package");
        sPackageManager = IPackageManager.Stub.asInterface(b);
        return sPackageManager;
    }

繼續深究,找出PMS中相關源碼。

@Override
    public ApplicationInfo getApplicationInfo(String packageName, int flags, int userId) {
        if (!sUserManager.exists(userId)) return null;
        enforceCrossUserPermission(Binder.getCallingUid(), userId, false, "get application info");
        // writer
        synchronized (mPackages) {
            PackageParser.Package p = mPackages.get(packageName);
            if (DEBUG_PACKAGE_INFO) Log.v(
                    TAG, "getApplicationInfo " + packageName
                    + ": " + p);
            if (p != null) {
                PackageSetting ps = mSettings.mPackages.get(packageName);
                if (ps == null) return null;
                // Note: isEnabledLP() does not apply here - always return info
                return PackageParser.generateApplicationInfo(
                        p, flags, ps.readUserState(userId), userId);
            }
            if ("android".equals(packageName)||"system".equals(packageName)) {
                return mAndroidApplication;
            }
            if ((flags & PackageManager.GET_UNINSTALLED_PACKAGES) != 0) {
                return generateApplicationInfoFromSettingsLPw(packageName, flags, userId);
            }
        }
        return null;
    }

可以看出會去mPackages中找,然而根本就找不到,因為根本就沒有安裝。

AssetManager

AssetManager這裡就不做贅述了,上一篇已經簡單看過,可以直接指定目錄。換言之,也就更加靈活。

從上面可以看出,AssetManager比getResourcesForApplication要靈活很多,使用場景也更廣。

免安裝運行Activity(上)

看完了前面的部分,我們知道可以通過DexClassLoader來加載類,通過AssetManager可以來加載資源。可是現在問題來了,怎麼運行一個未安裝APK中的Activity?Activity不僅有類有資源,最最重要的是,它有生命!。

我們先來看看按照之前的寫法會發生什麼狀況吧,首先在插件的PluginClass中加入啟動Activity的代碼如下:

    public void startPluginActivity(Context context, Class cls) {
        Intent intent=new Intent(context,cls);
        context.startActivity(intent);
    }

  public void startPluginActivity(Context context) {
        Intent intent=new Intent(context,PluginActivity.class);
        context.startActivity(intent);
    }

然後修改核心測試代碼,分別測試兩種形式。


    private void useDexClassLoader(String path){
        loadResources(path);
        File codeDir=getDir("dex", Context.MODE_PRIVATE);

        //創建類加載器,把dex加載到虛擬機中
        ClassLoader classLoader = new DexClassLoader(path,codeDir.getAbsolutePath() ,null,
                this.getClass().getClassLoader());
        //獲得包管理器
        PackageManager pm = getPackageManager();
        PackageInfo packageInfo=pm.getPackageArchiveInfo(path,PackageManager.GET_ACTIVITIES);
        String packageName=packageInfo.packageName;

        try {
            Class clazz = classLoader.loadClass(packageName+".PluginClass");
            Comm obj = (Comm) clazz.newInstance();
            obj.startPluginActivity(this,classLoader.loadClass(packageName+".PluginActivity"));
          //  obj.startPluginActivity(this);

        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

果然沒有想象中那麼輕松,直接報錯提示。

 android.content.ActivityNotFoundException: Unable to find explicit activity class {com.maplejaw.hotfix/com.maplejaw.hotplugin.PluginActivity}; have you declared this activity in your AndroidManifest.xml?

提示找不到Activity,是否在AndroidManifest.xml中聲明?說的也是,並沒有在宿主APK中進行聲明啊,插件APK的清單是沒有效果的。於是懷著滿滿的自信在AndroidManifest.xml中加入聲明。

  

筆者心想,這回應該可以了吧,再次運行測試。WTF!又報錯。

java.lang.RuntimeException: Unable to instantiate activity ComponentInfo{com.maplejaw.hotfix/com.maplejaw.hotplugin.PluginActivity}: java.lang.ClassNotFoundException: Didn't find class "com.maplejaw.hotplugin.PluginActivity" on path: DexPathList[[zip file "/data/app/com.maplejaw.hotfix-2/base.apk"],nativeLibraryDirectories=[/vendor/lib64, /system/lib64]]

從打印信息可以看出提示沒有找到該類。這就奇怪了,明明可以找到PluginClass類,為什麼提示找不到PluginActivity這個類呢?簡直沒有道理啊。

為了進行對比,筆者故意修改核心測試代碼去加載一個不存在的PluginClass2類,看看有什麼提示。

java.lang.ClassNotFoundException: Didn't find class "com.maplejaw.hotplugin.PluginClass2" on path: DexPathList[[zip file "/storage/emulated/0/2.apk"],nativeLibraryDirectories=[/vendor/lib64, /system/lib64]]

同樣提示找不到該類。
但是!!!注意看DexPathList這裡,它們指向的dex目錄居然不一樣。換言之,它們兩個的ClassLoader不是同一個。
我們先不想其他問題,暫時不去研究startActivity的源碼(下篇探索動態代理會進行研究)。我們先來想一個解決思路,有沒有一種方法可以將dex目錄指向到插件APK的dex?

替換ClassLoader

要更改dex目錄指向談何容易啊,更何況還要同時兼顧兩個dex目錄。幸虧ClassLoader遵循著雙親委托原則,讓這一切變得不是特別困難。

還記得我們在第一篇DexClassLoader中提到過,一個BaseClassLoader對應一個DexPathList 嗎?

public BaseDexClassLoader(String dexPath, File optimizedDirectory,String libraryPath, ClassLoader parent) {
        super(parent);
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }

那麼我們把啟動Activity的那個ClassLoader替換成我們的,不就間接的改變了dex目錄指向嗎?你可能會擔心,替換成我們的ClassLoader,那宿主APK中的類還找得到嗎?由於雙親委托原則,會首先從父ClassLoader中去找,只要我們的父ClassLoader是默認的系統ClassLoader即可。

所以,我們現在的任務是要把ClassLoader替換掉,翻了翻源碼,發現ClassLoader對象在LoadedApk中

LoadedApk

而ActivityThread中有著相關引用。<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwPjxpbWcgYWx0PQ=="這裡寫圖片描述" src="/uploadfile/Collfiles/20160530/20160530092133119.png" title="\" />

於是做了如下反射替換。

private void replaceClassLoader(ClassLoader dLoader,String resPath){
        try{
            String packageName = this.getPackageName();
            ClassLoader loader=ClassLoader.getSystemClassLoader();
            Class loadApkCls =loader.loadClass("android.app.LoadedApk");
            Class activityThreadCls =loader.loadClass("android.app.ActivityThread");

            //獲取ActivityThread對象
            Method currentActivityThreadMethod=activityThreadCls.getMethod("currentActivityThread");
            Object currentActivityThread= currentActivityThreadMethod.invoke(null);
            //反射獲取mPackages中的LoadedApk
            Field filed=activityThreadCls.getDeclaredField("mPackages");
            filed.setAccessible(true);
            Map mPackages= (Map) filed.get(currentActivityThread);
            WeakReference wr = (WeakReference) mPackages.get(packageName);
             //反射修改LoadedApk中的mClassLoader
            Field classLoaderFiled=loadApkCls.getDeclaredField("mClassLoader");
            classLoaderFiled.setAccessible(true);
            classLoaderFiled.set(wr.get(),dLoader);


        }catch(Exception e){
           e.printStackTrace();
        }

    }

插件Activity的代碼如下:

public class PluginActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_plugin);
        Log.i("JG",  "包名:"+getPackageName());
        Log.w("JG",  "代碼路徑:"+getPackageCodePath());
        Log.e("JG",  "資源路徑:"+getPackageResourcePath());

    }

    @Override
    protected void onStart() {
        super.onStart();
        Log.d("JG","onStart");
    }

    @Override
    protected void onResume() {
        super.onResume();
        Log.d("JG","onResume");
    }

    @Override
    protected void onPause() {
        super.onPause();
        Log.d("JG","onPause");
    }

    @Override
    protected void onStop() {
        super.onStop();
        Log.d("JG","onStop");
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        Log.d("JG","onDestroy");
    }

運行測試通過,Activity是能啟動了,生命周期完全正常,但是發現資源卻完全加載不了,一片白(也有加載到宿主界面的,那是因為資源id剛好和插件的重復)!控制台打印信息如下:
這裡寫圖片描述
可以看出代碼路徑和資源路徑全部指向了宿主APK,即使使用loadResources也完全沒有效果,因為一個Activity一個Context,我們的loadResources只對那個Activity的Context有效果。迫不得已,又去翻看了源碼,最後在上面的反射基礎中加入如下反射修改LoadedApk中的mResDir代碼。

 //反射修改LoadedApk中的資源目錄
            Field filed2=loadApkCls.getDeclaredField("mResDir");
            filed2.setAccessible(true);
            filed2.set(wr.get(),resPath);

測試,啟動成功,加載出插件的界面。查看控制台,發現成功修改資源目錄,生命周期完全正常。
image_1ajsv6rti1ocj1umj10un1kqc11b5l.png-34.1kB
但是呢,這種方法是有弊端的,因為反射導致它徹底改變了資源目錄,假如你要回到宿主Activity還要重新切換目錄才行。不由得想,要是資源也有雙親委托該有多好啊。

合並DexPathList

這種方式類似於熱修復方案。將插件的dexElements插入到系統的dexElements中,這樣我們啟動Activity時就不會提示找不到該類。在第一篇中,我們簡單看過DexPathList源碼,現在再來回顧下。
首先,一個ClassLoader一個DexPathList。
這裡寫圖片描述
然後,一個DexPathList中含有一個dexElements數組
這裡寫圖片描述
最後,加載類時從dexElements數組中遍歷。
這裡寫圖片描述

好了,思路很清晰,通過反射,將插件的dexElements與宿主的合並,並賦值給宿主的dexPathList。
實現方案如下:

    private void combinePathList(ClassLoader loader){
        //獲取系統的classloader
        PathClassLoader pathLoader = (PathClassLoader) getClassLoader();

        try {
            //反射dexpathlist
            Field pathListFiled = Class.forName("dalvik.system.BaseDexClassLoader").getDeclaredField("pathList");
            pathListFiled.setAccessible(true);
            //反射dexElements
            Field dexElementsFiled=Class.forName("dalvik.system.DexPathList").getDeclaredField("dexElements");
            dexElementsFiled.setAccessible(true);
            //獲取系統的pathList
            Object pathList1= pathListFiled.get(pathLoader);
            //獲取系統的dexElements
            Object dexElements1=dexElementsFiled.get(pathList1);

            //獲取插件的pathlist
            Object pathList2= pathListFiled.get(loader);
            //獲取插件的dexElements
            Object dexElements2=dexElementsFiled.get(pathList2);
            //合並dexElements
            Object combineDexElements=combineArray(dexElements1,dexElements2);
            //設置給系統的dexpathlist
            dexElementsFiled.set(pathList1,combineDexElements);

        } catch (IllegalArgumentException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }



   //合並兩個數組,返回一個新數組
    private static Object combineArray(Object arrayLhs, Object arrayRhs) {
        Class localClass = arrayLhs.getClass().getComponentType();
        int i = Array.getLength(arrayLhs);
        int j = i + Array.getLength(arrayRhs);
        Object result = Array.newInstance(localClass, j);
        for (int k = 0; k < j; ++k) {
            if (k < i) {
                Array.set(result, k, Array.get(arrayLhs, k));
            } else {
                Array.set(result, k, Array.get(arrayRhs, k - i));
            }
        }
        return result;
    }

測試通過,成功啟動Activity。
但是,同樣需要在清單文件注冊,同樣加載不了資源。仍然需要去反射替換掉LoadedApk中的資源目錄。

源碼下載地址:https://github.com/maplejaw/HotPluginDemo

最後

關於上面啟動免安裝Activity的方案,可以看出存在很明顯的缺陷,首先,需要在清單文件提前注冊,此外資源反射修改也很蛋疼。如果不想用反射,我們可以提前將資源內置於宿主中,或者純用JAVA代碼來寫。

但是,這兩種方案總歸很麻煩。有沒有更好的方案呢?沒錯,就是動態代理!
下一篇准備探索動態代理啟動Activity。

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