Android 插件化原理解析——廣播插件的實(shí)現(xiàn)與安裝apk原理解析

前言

android技術(shù)特別成熟了窜管,熱修復(fù),組件化......等框架已經(jīng)層出不窮韧涨,如果還僅限于使用框架牍戚,技術(shù)永遠(yuǎn)很難得到成長(zhǎng),只有我們懂得他的原理虑粥,能夠娓娓道來如孝,能夠自己動(dòng)手的寫出來,技術(shù)才會(huì)越來越好娩贷,與其想著未來怎么辦第晰,不如把握現(xiàn)在。這一篇文章教大家手寫出插件化框架,插件化技術(shù)是Android工程師必備的技術(shù)之一茁瘦,我們要懂其思想罗岖,知其原理。

那么在 android中腹躁,什么是「 插件化 」桑包,顧名思義啊,就是把一些核心復(fù)雜依賴度高的業(yè)務(wù)模塊封裝成獨(dú)立的插件纺非,然后根據(jù)不同業(yè)務(wù)需求進(jìn)行不同組合哑了,動(dòng)態(tài)進(jìn)行替換,可對(duì)插件進(jìn)行管理烧颖、更新弱左,后期對(duì)插件也可進(jìn)行版本管理等操作。在插件化中有兩個(gè)概念需要講解下:

宿主

所謂宿主炕淮,就是需要能提供運(yùn)行環(huán)境拆火,給資源調(diào)用提供上下文環(huán)境,一般也就是我們主 APK 涂圆,要運(yùn)行的應(yīng)用们镜,它作為應(yīng)用的主工程所在,實(shí)現(xiàn)了一套插件的加載和管理的框架润歉,插件都是依托于宿主的APK而存在的模狭。

插件

插件可以想象成每個(gè)獨(dú)立的功能模塊封裝為一個(gè)小的 APK ,可以通過在線配置和更新實(shí)現(xiàn)插件 APK 在宿主 APK 中的上線和下線踩衩,以及動(dòng)態(tài)更新等功能嚼鹉。

那么為何要使用插件化技術(shù),它有何優(yōu)勢(shì)驱富,能給我們帶來什么樣<typo id="typo-522" data-origin="好處" ignoretag="true">好處</typo>锚赤,這里簡(jiǎn)單列舉了以下幾點(diǎn):

讓用戶不用重新安裝 APK 就能升級(jí)應(yīng)用功能,減少發(fā)版本頻率褐鸥,增加用戶體驗(yàn)线脚。 提供一種快速修復(fù)線上 BUG 和更新的能力。 按需加載不同的模塊晶疼,實(shí)現(xiàn)靈活的功能配置酒贬,減少服務(wù)器對(duì)舊版本接口兼容壓力。 模塊化翠霍、解耦合锭吨、并行開發(fā)、 65535 問題寒匙。

簡(jiǎn)單描述一下入門的知識(shí)
首先我們要知道插件化技術(shù)是屬于比較復(fù)雜一個(gè)領(lǐng)域零如,復(fù)雜點(diǎn)在于它涉及知識(shí)點(diǎn)廣泛躏将,不僅僅是上層做應(yīng)用架構(gòu)能力,還要求我們對(duì) Android 系統(tǒng)底層知識(shí)需要有一定的認(rèn)知考蕾,這里簡(jiǎn)單羅列了其中會(huì)涉及的知識(shí)點(diǎn):

img

然后這一篇文章我?guī)Т蠹襾硎煜ぜ皩?shí)現(xiàn)廣播插件祸憋,廣播主要分兩種,一種動(dòng)態(tài)廣播,一種靜態(tài)廣播。

插件中動(dòng)態(tài)廣播的實(shí)現(xiàn):

啟動(dòng)插件中的動(dòng)態(tài)廣播其實(shí)和啟動(dòng)Activity和Service是一樣的流程.

首先我們需要在 通用庫里面新建一個(gè)接口,插件中的廣播都要實(shí)現(xiàn)此接口

public interface PluginInterfaceBroadcast {
    void attach(Context context);

    void onReceive(Context context, Intent intent);
}

然后在BaseActivity中重寫registerReceiversendBroadcast肖卧、unregisterReceiver,因?yàn)檫@兩個(gè)方法都用到了上下文(Context),我們需要用宿主(app)的Context來注冊(cè)和發(fā)送廣播.代碼如下:

@Override
    public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
        if (mActivity != null) {
            return mActivity.registerReceiver(receiver, filter);
        }
        return super.registerReceiver(receiver, filter);
    }

    @Override
    public void sendBroadcast(Intent intent) {
        if (null != mActivity) {
            mActivity.sendBroadcast(intent);
        } else {
            super.sendBroadcast(intent);
        }
    }
    @Override
    public void unregisterReceiver(BroadcastReceiver receiver) {
        if (null != mActivity) {
            mActivity.unregisterReceiver(receiver);
        } else {
            super.unregisterReceiver(receiver);
        }
    }

上述代碼其實(shí)就是,調(diào)用了宿主(app)的里面方法,就是啟動(dòng)了宿主定義好的第一個(gè)空殼的廣播,然后通過DexClassLoader反射插件中的廣播類,然后通過繼承的接口,來進(jìn)行方法的調(diào)用和參數(shù)的傳遞.

public class ProxyBroadcast extends BroadcastReceiver {
    private String className;

    private PluginInterfaceBroadcast bordcast;

    private static final String TAG = "ProxyBroadcast";

    public ProxyBroadcast(String name, Context context) {
        this.className = name;
        Class loadClass = null;
        try {
            loadClass = PluginManager.getInstance().getClassLoader().loadClass(className);
            Constructor constructor = loadClass.getConstructor(new Class[]{});
            bordcast = (PluginInterfaceBroadcast) constructor.newInstance(new Object[]{});
            bordcast.attach(context);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    //class --- object --- p
    @Override
    public void onReceive(Context context, Intent intent) {
        Log.e(TAG, "ProxyBroadcast onReceive: ");
        if (bordcast != null) {
            bordcast.onReceive(context, intent);
        }
    }
}

同理,在宿主方法中需要做一些處理,new ProxyBroadcast 然后注冊(cè)此廣播,實(shí)際上就是注冊(cè)了宿主的空殼的一個(gè)廣播:

 private List<ProxyBroadcast> proxyBroadcastList = new ArrayList<>();

    @Override
    public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
        //重寫真正注冊(cè)的是ProxyBroadcast 轉(zhuǎn)發(fā)
        IntentFilter filter1 = new IntentFilter();
        for (int i = 0; i < filter.countActions(); i++) {
            filter1.addAction(filter.getAction(i));
            Log.e(TAG, "sendBroadcast: 注冊(cè)插件的廣播 -> " + filter1.getAction(i));
        }
        ProxyBroadcast proxyBroadcast = new ProxyBroadcast(receiver.getClass().getName(), this);
        proxyBroadcastList.add(proxyBroadcast);
        return super.registerReceiver(proxyBroadcast, filter1);
    }

    @Override
    public void unregisterReceiver(BroadcastReceiver receiver) {
        if (proxyBroadcastList != null && proxyBroadcastList.size() > 0) {
            for (ProxyBroadcast proxyBroadcast : proxyBroadcastList) {
                if (proxyBroadcast.getClass().getName().equals(receiver.getClass().getName())) {
                    super.unregisterReceiver(proxyBroadcast);
                }
            }
        } else {
            super.unregisterReceiver(receiver);
        }
    }

我們來看一個(gè)插件中的廣播實(shí)現(xiàn),通過繼承PluginInterfaceBroadcast,宿主調(diào)用接口的方法:

public class MyReceive extends BroadcastReceiver implements PluginInterfaceBroadcast {
    @Override
    public void attach(Context context) {
        Toast.makeText(context, "注冊(cè)廣播成功", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onReceive(Context context, Intent intent) {
        Toast.makeText(context, "接收廣播成功", Toast.LENGTH_SHORT).show();
    }
}

這樣我們就實(shí)現(xiàn)了,插件動(dòng)態(tài)廣播的實(shí)現(xiàn),是不是<typo id="typo-4649" data-origin="的" ignoretag="true">的</typo>非常簡(jiǎn)單,幾乎沒有什么難點(diǎn),接下來我們來看插件靜態(tài)廣播如何來實(shí)現(xiàn)

插件靜態(tài)廣播實(shí)現(xiàn)原理:

我們知道任何插件都是沒有安裝到手機(jī)上的,靜態(tài)廣播是注冊(cè)在AndroidManifest中,那么,我們就不能通過上述那樣輕松的拿到類名進(jìn)行反射了,如下

 <receiver android:name=".StaticBroadcastReceiver">
            <intent-filter>
                <action android:name="com.prim.plugin.a" />
            </intent-filter>
        </receiver>

需要通過PMS來解析xml,拿到在xml注冊(cè)的類名,和intent-filter,然后和上面講的動(dòng)態(tài)廣播一樣,宿主動(dòng)態(tài)注冊(cè)插件中的靜態(tài)廣播,以達(dá)到這樣的效果.實(shí)際上宿主就是做了動(dòng)態(tài)注冊(cè)靜態(tài)廣播,都是天馬行空的想象.

APK安裝時(shí)做了什么呢?

  • -將apk文件復(fù)制到data/app目錄
  • ---使用PackageManager的installPackage接口
  • ---之后調(diào)用installPackageAsUser蚯窥。installPackageAsUser方法中主要完成兩件事情。
  • ------是權(quán)限檢查
  • ------是構(gòu)建InstallParams塞帐,然后發(fā)送INIT_COPY的msg,這個(gè)mHandler運(yùn)行在一個(gè)HandlerThread中,INIT_COPY主要是確保DefaultContainerService 已 bound,DefaultContainerService是一個(gè)應(yīng)用服務(wù)拦赠,具體負(fù)責(zé)實(shí)現(xiàn)APK等相關(guān)資源文件在內(nèi)部或外部存儲(chǔ)器上的存儲(chǔ)工作。而MCS_BOUND中則執(zhí)行 params.startCopy()
  • ------HandlerParams.startCopy該方法中除了檢查重試次數(shù)外只是簡(jiǎn)單的調(diào)用了handleStartCopy()及handleReturnCode()方法葵姥。 -解析apk信息
  • ---copy到data/app目錄的操作后,就到了 handleReturnCode,這個(gè)方法又跳轉(zhuǎn)到processPendingInstall()方法 ---這個(gè)方法有幾個(gè)關(guān)鍵步驟,一是installPackageLI(args, res);,這個(gè)方法具體執(zhí)行了解析package和后續(xù)操作,而再installPackageLI(args, res);執(zhí)行完畢 后會(huì)走到bm.restoreAtInstall(res.pkg.applicationInfo.packageName, token);,會(huì)調(diào)用backupservice的restoreAtInstall方法,而restoreAtInstall方法最 終又會(huì)調(diào)用PMS的finishPackageInstall()方法,完成安裝荷鼠。
  • -dexopt操作
  • -更新權(quán)限信息
  • -完成安裝,發(fā)送廣播(Intent.ACTION_PACKAGE_ADDED)

apk安裝時(shí)并有做很多操作,那么它是如何真正的加載靜態(tài)廣播? 真正的加載廣播,是在系統(tǒng)啟動(dòng)時(shí)發(fā)生的, 系統(tǒng)啟動(dòng)就可以理解成 將所有app重新安裝一遍到系統(tǒng)中榔幸,會(huì)重復(fù)上述過程.

PMS安裝APK原理

為了方便閱讀,我已經(jīng)將PackageManagerService 和PackageParser的源碼放到了代碼中,結(jié)合起來閱讀本文更容易理解.

PMS 全稱(PackageManagerService) ,當(dāng)安裝apk時(shí)會(huì)調(diào)用PackageManagerService的main方法

 public static final PackageManagerService main(Context context, Installer installer,
            boolean factoryTest, boolean onlyCore) {
        PackageManagerService m = new PackageManagerService(context, installer,
                factoryTest, onlyCore);
        ServiceManager.addService("package", m);
        return m;
    }

很顯然上述代碼,new PackageManagerService,我們接著看PackageManagerService的構(gòu)造方法.然后我們可以看到data app 這樣創(chuàng)建的目錄,著重關(guān)注mAppInstallDir,字面意思就是app的安裝路徑

public PackageManagerService(Context context, Installer installer,
            boolean factoryTest, boolean onlyCore) {
            .....
            synchronized (mPackages) {
            mHandlerThread = new ServiceThread(TAG,
                    Process.THREAD_PRIORITY_BACKGROUND, true /*allowIo*/);
            mHandlerThread.start();
            mHandler = new PackageHandler(mHandlerThread.getLooper());
            Watchdog.getInstance().addThread(mHandler, WATCHDOG_TIMEOUT);
            //TODO
            File dataDir = Environment.getDataDirectory();
            mAppDataDir = new File(dataDir, "data");
            mAppInstallDir = new File(dataDir, "app");
            mAppLib32InstallDir = new File(dataDir, "app-lib");
            mAsecInternalPath = new File(dataDir, "app-asec").getPath();
            mUserAppDataDir = new File(dataDir, "user");
            mDrmAppPrivateInstallDir = new File(dataDir, "app-private");
            ......
             if (!mOnlyCore) {
                EventLog.writeEvent(EventLogTags.BOOT_PROGRESS_PMS_DATA_SCAN_START,
                        SystemClock.uptimeMillis());
                scanDirLI(mAppInstallDir, 0, scanFlags, 0);

                scanDirLI(mDrmAppPrivateInstallDir, PackageParser.PARSE_FORWARD_LOCK,
                        scanFlags, 0);
                        .....
                        }
            }

接著會(huì)調(diào)用scanDirLI去掃面apk文件,點(diǎn)擊去看一下它做了什么? 內(nèi)部有調(diào)用scanPackageLI方法.

private void scanDirLI(File dir, 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;
            }
            try {
                scanPackageLI(file, parseFlags | PackageParser.PARSE_MUST_BE_APK,
                        scanFlags, currentTime, null);
            } catch (PackageManagerException e) {
                Slog.w(TAG, "Failed to parse " + file + ": " + e.getMessage());

                // Delete invalid userdata apps
                if ((parseFlags & PackageParser.PARSE_IS_SYSTEM) == 0 &&
                        e.error == PackageManager.INSTALL_FAILED_INVALID_APK) {
                    logCriticalInfo(Log.WARN, "Deleting invalid package at " + file);
                    if (file.isDirectory()) {
                        FileUtils.deleteContents(file);
                    }
                    file.delete();
                }
            }
        }
        }

接著點(diǎn)scanPackageLI,從字面意思上看是解析包,我們看看它做了什么?返回了一個(gè)Package,這個(gè)Package是不是我們想要的呢?

 private PackageParser.Package scanPackageLI(File scanFile, int parseFlags, int scanFlags,
            long currentTime, UserHandle user) throws PackageManagerException {
....
        final PackageParser.Package pkg;
        try {
            pkg = pp.parsePackage(scanFile, parseFlags);
        } catch (PackageParserException e) {
            throw PackageManagerException.from(e);
        }

        .....
        }

看一下Package 里面有什么? 很顯然它是我們想要的Package存放的就是AndroidManifest.xml 注冊(cè)的四大組件.

 public final static class Package {
 ....
        public final ArrayList<Activity> activities = new ArrayList<Activity>(0);
        public final ArrayList<Activity> receivers = new ArrayList<Activity>(0);
        public final ArrayList<Provider> providers = new ArrayList<Provider>(0);
        public final ArrayList<Service> services = new ArrayList<Service>(0);
        ......
        }

注意這里的Activity不是四大組件的Activity而是PackageParser的一個(gè)內(nèi)部類,activity 與receivers 在 AndroidManifest里面屬性都差不多一樣,很顯然Google復(fù)用了Activity.

我們來看一下Activity里面存放了什么?

  public final static class Activity extends Component<ActivityIntentInfo> {
        public final ActivityInfo info;

        public Activity(final ParseComponentArgs args, final ActivityInfo _info) {
            super(args, _info);
            info = _info;
            info.applicationInfo = args.owner.applicationInfo;
        }

        public void setPackageName(String packageName) {
            super.setPackageName(packageName);
            info.packageName = packageName;
        }

       .......
    }

Activity中只存放了ActivityInfo,繼續(xù)點(diǎn)進(jìn)去ActivityInfo中是否有我們想要的類名等信息

ActivityInfo extends ComponentInfo 
ComponentInfo extends PackageItemInfo
/**
     * Public name of this item. From the "android:name" attribute.
     */
    public String name;

PackageItemInfo中找到了這樣的一個(gè)屬性,終于找到了我們想要的類名了.只要我們拿到ActivityInfo就可以拿到類名

<receiver android:name=".StaticBroadcastReceiver">
            <intent-filter>
                <action android:name="com.prim.plugin.a" />
            </intent-filter>
        </receiver>

但是還忽略了一點(diǎn),intent-filter 還沒有找到,從代碼中看

final static class Activity extends Component<ActivityIntentInfo>

 public static class Component<II extends IntentInfo> {
        public final Package owner;
        public final ArrayList<II> intents;
        public final String className;
        public Bundle metaData;
        }

ArrayList intents 看起來像是intent-filter,II泛型 --> IntentInfo

 public static class IntentInfo extends IntentFilter {
        public boolean hasDefault;
        public int labelRes;
        public CharSequence nonLocalizedLabel;
        public int icon;
        public int logo;
        public int banner;
        public int preferred;
    }

可以看到IntentInfo 繼承 IntentFilter,找到了IntentFilter.

中途總結(jié)

從上述代碼中,我們可以知道通過,parsePackage解析包,得到Package,就可以拿到AndroidManifeast的信息.

PackageParser pp = new PackageParser();

final PackageParser.Package pkg;
//解析apk 得到pkg
pkg = pp.parsePackage(scanFile, parseFlags);

但是很不幸的是,Google將這個(gè)類寫成了@hide 隱藏的API那我們就只能通過反射去獲取了,感覺瞬間腦殼痛了.

沒有辦法嘞,我們就只能硬來咯

首先我們實(shí)例化PackageParser然后調(diào)用parsePackage得到Package

 //反射獲取解析apk包的類
            Class packageParserClass = Class.forName("android.content.pm.PackageParser");
            //獲取方法
            Method parsePackage = packageParserClass.getDeclaredMethod("parsePackage",
                    File.class, int.class);
            //實(shí)例化PackageParser類
            Object packageParser = packageParserClass.newInstance();
            //Package 得到
            Object packageObj = parsePackage.invoke(packageParser, new File(absolutePath), PackageManager.GET_ACTIVITIES);
            //拿到注冊(cè)的靜態(tài)廣播

然后通過Package拿到xml注冊(cè)的靜態(tài)廣播

//拿到注冊(cè)的靜態(tài)廣播
            Field receiversField = packageObj.getClass().getDeclaredField("receivers");
            //獲取List<Activity>
            List receivers = (List) receiversField.get(packageObj);

然后循環(huán)Activity,拿到類名和intent-fliter

//循環(huán)receivers
            for (Object activity : receivers) {
                //拿到ActivityInfo
                ActivityInfo info = (ActivityInfo) generateReceiverInfo.invoke(packageParser, activity, 0, defaltUserState, userId);
                //根據(jù)ActivityInfo允乐,拿到BroadCastReceiver
                BroadcastReceiver broadcastReceiver = (BroadcastReceiver) classLoader.loadClass(info.name).newInstance();
                //拿到intentFilter
                List<? extends IntentFilter> intentFilters = (List<? extends IntentFilter>) intentsField.get(activity);
                for (IntentFilter filter : intentFilters) {
                    //動(dòng)態(tài)注冊(cè)插件中的靜態(tài)廣播
                    context.registerReceiver(broadcastReceiver, filter);
                }
            }

這樣看起來,實(shí)現(xiàn)思路很簡(jiǎn)單啊,一臉的懵逼啊.


SystemService -> main

->scanDirLI 掃描apk的文件 --》scanPackageLI 解析apk PackageParser -> parsePackage get Package (一個(gè)apk對(duì)應(yīng)一個(gè)package)

-> parseBaseApk loadApkIntoAssetManager -> parseBaseApk -> parseBaseApplication -> parseActivity -> parseIntent

核心完整代碼如下:

/**
     * 解析xml靜態(tài)注冊(cè)的廣播
     *
     * @param context
     * @param absolutePath
     */
    private void parserReceive(Context context, String absolutePath) {
        try {
            //反射獲取解析apk包的類
            Class packageParserClass = Class.forName("android.content.pm.PackageParser");
            //獲取方法
            Method parsePackage = packageParserClass.getDeclaredMethod("parsePackage",
                    File.class, int.class);
            //實(shí)例化PackageParser類
            Object packageParser = packageParserClass.newInstance();
            //Package 得到
            Object packageObj = parsePackage.invoke(packageParser, new File(absolutePath), PackageManager.GET_ACTIVITIES);
            //拿到注冊(cè)的靜態(tài)廣播
            Field receiversField = packageObj.getClass().getDeclaredField("receivers");
            //獲取List<Activity>
            List receivers = (List) receiversField.get(packageObj);
            //public final static class Activity extends Component<ActivityIntentInfo>
            //獲取Component
            Class<?> componentClass = Class.forName("android.content.pm.PackageParser$Component");
            //獲取intents
            Field intentsField = componentClass.getDeclaredField("intents");
            //generatePackageInfo(PackageParser.Package p,
            //            int gids[], int flags, long firstInstallTime, long lastUpdateTime,
            //            HashSet<String> grantedPermissions, PackageUserState state, int userId)

            // 調(diào)用generateActivityInfo 方法, 把PackageParser.Activity 轉(zhuǎn)換成
            Class<?> packageParser$ActivityClass = Class.forName("android.content.pm.PackageParser$Activity");
            // generateActivityInfo方法
            Class<?> packageUserStateClass = Class.forName("android.content.pm.PackageUserState");
            Object defaltUserState = packageUserStateClass.newInstance();

            Method generateReceiverInfo = packageParserClass.getDeclaredMethod("generateActivityInfo",
                    packageParser$ActivityClass, int.class, packageUserStateClass, int.class);
            //獲取userID
            Class<?> userHandler = Class.forName("android.os.UserHandle");
            Method getCallingUserIdMethod = userHandler.getDeclaredMethod("getCallingUserId");
            int userId = (int) getCallingUserIdMethod.invoke(null);

            //循環(huán)receivers
            for (Object activity : receivers) {
                //拿到ActivityInfo
                ActivityInfo info = (ActivityInfo) generateReceiverInfo.invoke(packageParser, activity, 0, defaltUserState, userId);
                //根據(jù)ActivityInfo,拿到BroadCastReceiver
                BroadcastReceiver broadcastReceiver = (BroadcastReceiver) classLoader.loadClass(info.name).newInstance();
                //拿到intentFilter
                List<? extends IntentFilter> intentFilters = (List<? extends IntentFilter>) intentsField.get(activity);
                for (IntentFilter filter : intentFilters) {
                    //動(dòng)態(tài)注冊(cè)插件中的靜態(tài)廣播
                    context.registerReceiver(broadcastReceiver, filter);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

插件中的靜態(tài)廣播就不需要實(shí)現(xiàn)接口PluginInterfaceBroadcast

public class StaticBroadcastReceiver extends BroadcastReceiver {

    private static final String ACTION = "com.prim.plugin.host";

    @Override
    public void onReceive(Context context, Intent intent) {
        Toast.makeText(context, "我是插件削咆,收到發(fā)送的廣播,我將向宿主發(fā)送廣播", Toast.LENGTH_SHORT).show();
        //接收到廣播牍疏,然后給宿主發(fā)送廣播
        context.sendBroadcast(new Intent(ACTION));
    }
}

最后

如果前面我說的還有點(diǎn)不理解,我在舉個(gè)例子:

(以下的靜A 表示靜態(tài)廣播接收器态辛,同理動(dòng)B麸澜。)

1、 靜A (優(yōu)先級(jí)1)

2奏黑、 動(dòng)B(優(yōu)先級(jí)1)

3、 靜C (優(yōu)先級(jí)2编矾,后掃描)

4熟史、靜D (優(yōu)先級(jí)2,先掃描)

5 窄俏、動(dòng)E (優(yōu)先級(jí)2蹂匹,先注冊(cè))

6、 動(dòng)F (優(yōu)先級(jí)2凹蜈,后注冊(cè))

當(dāng)來了一個(gè) 有序廣播限寞,接收順序如下:動(dòng)E > 動(dòng)F > 靜D > 靜C > 動(dòng)B > 靜A

當(dāng)來了一個(gè) 普通廣播,接收順序如下:動(dòng)E > 動(dòng)F > 動(dòng)B > 靜D > 靜C > 靜A

Android插件化的發(fā)展方向

Android插件化方向主要有2個(gè)方向:

  • 1.結(jié)合組件化技術(shù)仰坦,成為一個(gè)大中型app的基礎(chǔ)框架履植。
  • 2.完全模擬app運(yùn)行環(huán)境的沙盒系統(tǒng)。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末悄晃,一起剝皮案震驚了整個(gè)濱河市玫霎,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖庶近,帶你破解...
    沈念sama閱讀 211,423評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件翁脆,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡鼻种,警方通過查閱死者的電腦和手機(jī)反番,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,147評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來叉钥,“玉大人罢缸,你說我怎么就攤上這事≌勇拢” “怎么了祖能?”我有些...
    開封第一講書人閱讀 157,019評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)蛾洛。 經(jīng)常有香客問我养铸,道長(zhǎng),這世上最難降的妖魔是什么轧膘? 我笑而不...
    開封第一講書人閱讀 56,443評(píng)論 1 283
  • 正文 為了忘掉前任钞螟,我火速辦了婚禮,結(jié)果婚禮上谎碍,老公的妹妹穿的比我還像新娘鳞滨。我一直安慰自己,他們只是感情好蟆淀,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,535評(píng)論 6 385
  • 文/花漫 我一把揭開白布拯啦。 她就那樣靜靜地躺著,像睡著了一般熔任。 火紅的嫁衣襯著肌膚如雪褒链。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,798評(píng)論 1 290
  • 那天疑苔,我揣著相機(jī)與錄音甫匹,去河邊找鬼。 笑死惦费,一個(gè)胖子當(dāng)著我的面吹牛兵迅,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播薪贫,決...
    沈念sama閱讀 38,941評(píng)論 3 407
  • 文/蒼蘭香墨 我猛地睜開眼恍箭,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了后雷?” 一聲冷哼從身側(cè)響起季惯,我...
    開封第一講書人閱讀 37,704評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤吠各,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后勉抓,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體贾漏,經(jīng)...
    沈念sama閱讀 44,152評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,494評(píng)論 2 327
  • 正文 我和宋清朗相戀三年藕筋,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了纵散。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,629評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡隐圾,死狀恐怖伍掀,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情暇藏,我是刑警寧澤蜜笤,帶...
    沈念sama閱讀 34,295評(píng)論 4 329
  • 正文 年R本政府宣布,位于F島的核電站盐碱,受9級(jí)特大地震影響把兔,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜瓮顽,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,901評(píng)論 3 313
  • 文/蒙蒙 一县好、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧暖混,春花似錦缕贡、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,742評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至贮配,卻和暖如春禀酱,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背牧嫉。 一陣腳步聲響...
    開封第一講書人閱讀 31,978評(píng)論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留减途,地道東北人酣藻。 一個(gè)月前我還...
    沈念sama閱讀 46,333評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像鳍置,于是被迫代替她去往敵國和親辽剧。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,499評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容