佔位式插件化之加載靜態廣播

接着前幾篇文章來:由於插件中的廣播是在manifest中配置的,所以就不能使用上一篇中的方法來註冊廣播了,首先我們需要了解一下APK的解析原理

第一步我們要知道靜態廣播是什麼時候註冊的?

在手機開機的時候,系統誰掃描所有的app,在重新安裝一遍,這也是爲啥手機開機會這麼慢,這時候系統會去解析AndroidManifest文件,解析的過程中遇到靜態廣播後就會自動註冊

第二步我們來看一下應用的安裝目錄

主要有三個目錄

/data/app

該文件夾存放着系統中安裝的第三方應用的 apk 文件,當我們調試一個app的時候,可以看到控制檯輸出的內容,有一項是
uploading ……就是上傳我們的apk到這個文件夾,上傳成功之後纔開始安裝。Android 中應用的安裝就是將應用的安裝包原封不動地拷貝到 /data/app 目錄下,每個應用安裝包本質上就是一個 zip 格式的壓縮文件。爲了提升應用的啓動效率,Android 會將解壓出來的 dex 格式的應用代碼文件解析提取後,緩存在 /data/dalvik-cache 目錄下。

/data/data

該文件夾存放存儲包私有數據,對於設備中每一個安裝的 App,系統都會在內部存儲空間的 data/data 目錄下以應用包名爲名字自動創建與之對應的文件夾。
用戶卸載 App 時,系統自動刪除 data/data 目錄下對應包名的文件夾及其內容。

data/dalvik-cache

虛擬機去加載執行指令

通過上面的解釋,可以知道,我們應該分析data/app這個目錄,手機開機的時候就會掃描這個目錄,來解析apk中的配置信息。

然後就開始看看系統是怎麼來解析apk文件的,系統中的包的解析都是通過PackageManagerService這個類來解析的,當系統啓動的時候,首先啓動Linux內核驅動,然後啓動init進程,然後啓動zygote孵化進程,在然後啓動SystemServer進程,然後啓動PackageManagerService。

所以我們去PackageManagerService這個類中看看系統是怎麼解析data/app中的 apk文件的

下面的源碼是基於Android9.0的,每個版本的源碼可能不一樣

Ok 現在來到PackageManagerService這個類中

我們從文件目錄入手,可以看到它有幾個靜態的成員變量

     /** Directory where installed applications are stored */
    private static final File sAppInstallDir =
            new File(Environment.getDataDirectory(), "app");
    /** Directory where installed application's 32-bit native libraries are copied. */
    private static final File sAppLib32InstallDir =
            new File(Environment.getDataDirectory(), "app-lib");
    /** Directory where code and non-resource assets of forward-locked applications are stored */
    private static final File sDrmAppPrivateInstallDir =
            new File(Environment.getDataDirectory(), "app-private"); 

其中第一個sAppInstallDir就是我們要找的安裝目錄data/app,所以從這裏入手,看看系統是怎麼解析apk文件的。

全局搜索sAppInstallDir就可以找到scanDirTracedLI這個方法,從名字也能看出,它就是掃描該目錄。內部也會解析manifest文件,所以從這裏開始分析,我們跟隨掃描的方法一步一步的往下看。

  private void scanDirTracedLI(File scanDir, final int parseFlags, int scanFlags, long currentTime) {
        Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "scanDir [" + scanDir.getAbsolutePath() + "]");
        try {
            scanDirLI(scanDir, parseFlags, scanFlags, currentTime);
        } finally {
            Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
        }
    }
 private void scanDirLI(File scanDir, int parseFlags, int scanFlags, long currentTime) {
     ...
         for (File file : files) {
                final boolean isPackage = (isApkFile(file) || file.isDirectory())
                        && !PackageInstallerService.isStageName(file.getName());
                if (!isPackage) {
                    // Ignore entries which are not packages
                    continue;
                }
                //開啓線程池來解析
                parallelPackageParser.submit(file, parseFlags);
                fileCount++;
            }
     ...
 }
 //mService是線程池
 public void submit(File scanFile, int parseFlags) {
        mService.submit(() -> {
            ParseResult pr = new ParseResult();
            Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "parallel parsePackage [" + scanFile + "]");
            try {
                PackageParser pp = new PackageParser();
                pp.setSeparateProcesses(mSeparateProcesses);
                pp.setOnlyCoreApps(mOnlyCore);
                pp.setDisplayMetrics(mMetrics);
                pp.setCacheDir(mCacheDir);
                pp.setCallback(mPackageParserCallback);
                pr.scanFile = scanFile;
                //解析包
                pr.pkg = parsePackage(pp, scanFile, parseFlags);
            } catch (Throwable e) {
                pr.throwable = e;
            } finally {
                Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
            }
            try {
                mQueue.put(pr);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                mInterruptedInThread = Thread.currentThread().getName();
            }
        });
    }
  @VisibleForTesting
    protected PackageParser.Package parsePackage(PackageParser packageParser, File scanFile,
            int parseFlags) throws PackageParser.PackageParserException {
        return packageParser.parsePackage(scanFile, parseFlags, true /* useCaches */);
    }

這一路跟隨,最後到了PackageParser這個類中的parsePackage方法。

//packageFile文件包的路徑
 public Package parsePackage(File packageFile, int flags) throws PackageParserException {
        return parsePackage(packageFile, flags, false /* useCaches */);
    }
public Package parsePackage(File packageFile, int flags, boolean useCaches)
            throws PackageParserException {
        Package parsed = useCaches ? getCachedResult(packageFile, flags) : null;
        if (parsed != null) {
            return parsed;
        }

        long parseTime = LOG_PARSE_TIMINGS ? SystemClock.uptimeMillis() : 0;
        if (packageFile.isDirectory()) {
            parsed = parseClusterPackage(packageFile, flags);
        } else {
            parsed = parseMonolithicPackage(packageFile, flags);
        }
         ...
        return parsed;
    }

到這裏我們就知道系統是通過parsePackage這個方法來解析apk文件的,那麼我們是不是可以反射得到這個方法來解析我們自己的apk包呢?當然可以啦,最後我們只要拿到Package這個對象就行了,這個Package對象是parsePackage的內部類,它裏面包含了AndroidManifest中的所有信息包含Permission,Activity,Service,Provider,廣播等等。能拿到靜態廣播的信息就可以給它註冊了。

所以現在接着前兩篇文章來,在PluginManager類中添加一個解析apk獲取廣播並註冊的方法

/**
     * 反射系統源碼來解析自己的apk 註冊廣播
     */
    public void parseApkGetReceiver(){
        try {
            File file = new File(Environment.getExternalStorageDirectory()+File.separator+"p.apk");
            if(!file.exists()){
                Log.i(TAG,"插件包不存在");
            }
            //執行系統 PackageParser中的parsePackage方法來解析
            Class<?> packageParserClass = Class.forName("android.content.pm.PackageParser");
            Object packageParser = packageParserClass.newInstance();

            Method parsePackage = packageParserClass.getMethod("parsePackage",File.class,int.class);
            //mPackage就是PackageParser中的Package類
            Object mPackage = parsePackage.invoke(packageParser, file, PackageManager.GET_ACTIVITIES);
            //分析 mPackage拿到裏面的廣播的集合
            Field receiversField = mPackage.getClass().getDeclaredField("receivers");
            //本質是ArrayList集合  public final ArrayList<Activity> receivers = new ArrayList<Activity>(0);
            Object receivers = receiversField.get(mPackage);

            ArrayList list = (ArrayList) receivers;
            //集合內部的元素activity是PackageParse的內部類 是一個廣播的封裝類
            for (Object activity : list) {
                 //拿到intentfilter
                Class<?> componentClass = Class.forName("android.content.pm.PackageParser$Component");
                Field intentsFile = componentClass.getDeclaredField("intents");
                ArrayList<IntentFilter> intents = (ArrayList) intentsFile.get(activity);

                Class<?> packageUserState = Class.forName("android.content.pm.PackageUserState");
                Class<?> userHandle = Class.forName("android.os.UserHandle");
                int  userId = (int) userHandle.getDeclaredMethod("getCallingUserId").invoke(null);

                //拿到廣播的全類名
                Method generateActivityInfoMethod = packageParserClass.
                        getDeclaredMethod("generateActivityInfo", activity.getClass(),
                                int.class,packageUserState,int.class);
                generateActivityInfoMethod.setAccessible(true);
                ActivityInfo activityInfo = (ActivityInfo) generateActivityInfoMethod.invoke(null, activity, 0, packageUserState.newInstance(),
                        userId);
                //插件包中的廣播的全類名
                String receiverClassName = activityInfo.name;
                Class<?> receiverClass = getClassLoader().loadClass(receiverClassName);
                BroadcastReceiver  broadcastReceiver = (BroadcastReceiver) receiverClass.newInstance();
                for (IntentFilter intentFilter : intents) {
                    //註冊廣播
                    mContext.registerReceiver(broadcastReceiver,intentFilter);
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }

寫了這麼多反射的代碼,其實我們主要的目的只有兩個
第一個拿到intentFilter,第二個拿到broadcastReceiver, 最後調用mContext.registerReceiver方法註冊廣播。其餘代碼都是爲了找到這兩個參數來服務的。

在插件包中定義一個廣播並註冊到manifest中

public class PluginStaticReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {

        Toast.makeText(context, "我是靜態註冊的廣播,我收到廣播啦", Toast.LENGTH_SHORT).show();

    }
}
 <receiver android:name=".PluginStaticReceiver">

            <intent-filter>

                <action android:name="plugin.package.PluginStaticReceiver" />

            </intent-filter>

 </receiver>

最後在MainActivity中加兩個按鈕,加載註冊廣播併發送廣播

public void loadStaticReceiver(View view) {
        PluginManager.getInstance(this).parseApkGetReceiver();
    }

    public void sendStaticReceiver(View view) {
        Intent intent = new Intent();
        intent.setAction("plugin.package.PluginStaticReceiver");
        sendBroadcast(intent);
    }

效果:
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章