Android 源碼系列之通過反射解決在HuaWei手機出現Register too many Broadcast Receivers的crash

       轉載請註明出處:http://blog.csdn.net/llew2011/article/details/79054457

       Android開發適配問題一直是一個讓人頭疼的話題,由於國內很多廠商都有對原生Android系統做不同的定製,結果導致適配起來很麻煩。印象最深的一個適配是讓Notification的背景色做到和系統通知欄背景色一致,然後就是想各種辦法做適配……近來在Bugly上查看統計APP的crash日誌的時候發現有一個crash日誌很詭異,該crash只發生在HuaWei手機上,截取部分Crash日誌如下所示:

java.lang.AssertionError:Register too many Broadcast Receivers
	android.app.LoadedApk.checkRecevierRegisteredLeakLocked(LoadedApk.java:1010)
	android.app.LoadedApk.getReceiverDispatcher(LoadedApk.java:1038)
	android.app.ContextImpl.registerReceiverInternal(ContextImpl.java:1476)
	android.app.ContextImpl.registerReceiver(ContextImpl.java:1456)
	android.app.ContextImpl.registerReceiver(ContextImpl.java:1450)
	android.content.ContextWrapper.registerReceiver(ContextWrapper.java:586)
	com.tencent.sharp.jni.TraeAudioManager$TraeAudioManagerLooper.void _post_stopService()(TraeAudioManager.java:1982)
	com.tencent.sharp.jni.TraeAudioManager$TraeAudioManagerLooper.void stopService()(TraeAudioManager.java:1628)
	com.tencent.sharp.jni.TraeAudioManager$TraeAudioManagerLooper$2.void handleMessage(android.os.Message)(TraeAudioManager.java:1695)
	android.os.Handler.dispatchMessage(Handler.java:105)
	android.os.Looper.loop(Looper.java:156)
	com.tencent.sharp.jni.TraeAudioManager$TraeAudioManagerLooper.void run()(TraeAudioManager.java:1891)
       根據日誌信息看到拋出的異常爲:Register too many Broadcast Receivers,翻譯過來就是註冊的BroadcastReceiver太多導致的。根據調用的棧信息,是TraeAudioManager類的內部類TraeAudioManager的_post_stopService()方法內註冊BroadcastReceiver過多導致應用crash的。所以我們猜測該crash很有可能是在_post_stopService()方法內部註冊BroadcastReceiver前沒有進行反註冊操作導致的。由於TraeAudioManager類是鵝廠SDK中的類,因此只能反編譯查看TraeAudioManager類的實現方式,反編譯後的TraeAudioManager.class主要代碼如下;
public class TraeAudioManager extends BroadcastReceiver {

    // 省略部分代碼
    
    class TraeAudioManagerLooper extends Thread {
        
        // 省略部分代碼

        public TraeAudioManagerLooper(TraeAudioManager var2) {
            this._parent = var2;
            this.start();
            // 省略部分代碼
        }

        void stopService() {
            AudioDeviceInterface.LogTraceEntry(" _enabled:" + (this._enabled?"Y":"N") + " activeMode:" + TraeAudioManager.this._activeMode);
            if(this._enabled) {

                // 省略部分代碼

                this._post_stopService();

                // 省略部分代碼
            }
        }

        public void run() {
            Looper.prepare();
            this.mMsgHandler = new Handler() {
                public void handleMessage(Message var1) {

                    // 省略部分代碼

                    if(var1.what == '耄') {
                        TraeAudioManagerLooper.this.startService(var6);
                    } else if(!TraeAudioManagerLooper.this._enabled) {

                        Intent var7 = new Intent();
                        TraeAudioManager.this.sendResBroadcast(var7, var6, 1);
                    } else {
                        switch(var1.what) {
                        case 32773:
                            TraeAudioManagerLooper.this.stopService();
                            break;
                    }

                }
            };

            // 省略部分代碼

        }

        void _post_stopService() {
            try {
                if(TraeAudioManager.this._bluetoothCheck != null) {
                    TraeAudioManager.this._bluetoothCheck.release();
                }

                TraeAudioManager.this._bluetoothCheck = null;
                if(TraeAudioManager.this._context != null) {
                    TraeAudioManager.this._context.unregisterReceiver(this._parent);// 先反註冊廣播接收器
                    IntentFilter var1 = new IntentFilter();
                    var1.addAction("com.tencent.sharp.ACTION_TRAEAUDIOMANAGER_REQUEST");
                    TraeAudioManager.this._context.registerReceiver(this._parent, var1);// 再註冊廣播接收器
                }
            } catch (Exception var2) {
                ;
            }

        }

        // 省略部分代碼
    }

    // 省略部分代碼
}
       從反編譯後的TraeAudioManager類來看,_post_stopService()方法內部在註冊BroadcastReceiver之前都有反註冊BroadcastReceiver的操作,並且_post_stopService()方法又加上了try-catch操作,理論上來說通過這兩層驗證不應該再發生crash了,但事實真的crash了……通過在_post_stopService()方法內部整體添加try-catch的操作我們可以推斷:鵝廠SDK的研發童靴也清楚該處會拋異常(在HuaWei手機上會crash),所以他們添加了try-catch試圖捕獲該異常。但是我們回頭再仔細看一下crash日誌信息,發現拋出的異常名稱是AssertionError,該異常類型是Error類型,而_post_stopService()方法內部添加的try-catch()捕獲的異常是Exception,清楚Java異常捕獲機制的童靴應該清楚這兩個異常類型根本就不是同一類型,因此catch中定義的Exception類型是不能捕獲AssertionError異常的,很顯然鵝廠的SDK研發童靴忽略了這一點。

       既然_post_stopService()方法內部的try-catch操作是無效的,那麼我們就可以藉助上篇文章Android 源碼系列之<十七>自定義Gradle Plugin,優雅的解決第三方Jar包中的bug開發的BytecodeFixer插件來對該方法做修復,也就是添加捕獲Throwable所有異常的try-catch代碼塊。如果有小夥伴不清楚該插件的使用請閱讀上篇文章。引入插件,添加配置如下所示:

apply plugin: 'com.llew.bytecode.fix'

bytecodeFixConfig {

    enable true

    logEnable = true

    keepFixedJarFile = true

    keepFixedClassFile = true

    fixConfig = [
            'com.tencent.ilivesdk.core.impl.ILVBRoom##quitIMGroup()##if (null != super.mOption && super.mOption.isIMSupport()) {com.tencent.ilivesdk.core.ILiveLog.ki(TAG, "quitIMGroup", new com.tencent.ilivesdk.core.ILiveLog.LogExts().put("isHost", isHost));if (isHost) {com.tencent.ilivesdk.ILiveSDK.getInstance().getGroupEngine().deleteGroup(getIMGroupId(), null);} else {com.tencent.ilivesdk.ILiveSDK.getInstance().getGroupEngine().quitGroup(getIMGroupId(), null);};chatRoomId = null;};##-1',
            'com.tencent.ilivesdk.adapter.avsdk_impl.AVSDKContext$AVCreateContextCallBack##onComplete(int,java.lang.String)##{}##0',
            'com.tencent.ilivesdk.adapter.avsdk_impl.AVSDKContext##changeRole(java.lang.String,com.tencent.ilivesdk.ILiveCallBack)##{}##0',
            'com.tencent.av.sdk.NetworkHelp##getMobileAPInfo(android.content.Context,int)##if(android.content.pm.PackageManager.PERMISSION_GRANTED != $1.checkPermission(android.Manifest.permission.READ_PHONE_STATE, android.os.Process.myPid(), android.os.Process.myUid())){return new com.tencent.av.sdk.NetworkHelp.APInfo();}##0',
            'com.tencent.sharp.jni.TraeAudioManager$TraeAudioManagerLooper##_post_stopService()##{}##0',// 對_post_stopService()添加try-catch(Throwable)操作
    ]
}
       根據以上的配置,就可以對_post_stopService()方法添加try-catch(Throwable)代碼了,運行項目,修復後的_post_stopService()方法如下所示:
public class TraeAudioManager extends BroadcastReceiver {

    // 省略部分代碼
    
    class TraeAudioManagerLooper extends Thread {
        
        // 省略部分代碼

        void _post_stopService() {
            try {
                try {
                    if(TraeAudioManager.this._bluetoothCheck != null) {
                        TraeAudioManager.this._bluetoothCheck.release();
                    }

                    TraeAudioManager.this._bluetoothCheck = null;
                    if(TraeAudioManager.this._context != null) {
                        TraeAudioManager.this._context.unregisterReceiver(this._parent);
                        IntentFilter var1 = new IntentFilter();
                        var1.addAction("com.tencent.sharp.ACTION_TRAEAUDIOMANAGER_REQUEST");
                        TraeAudioManager.this._context.registerReceiver(this._parent, var1);
                    }
                } catch (Exception var3) {
                    ;
                }

            } catch (Throwable var4) {
                var4.printStackTrace();
            }
        }

        // 省略部分代碼
    }

    // 省略部分代碼
}
       現在我們利用了BytecodeFixer插件已經成功的對鵝廠SDK內部拋出的異常進行了捕獲操作,之後應用就不會在HuaWei手機上crash了,但是這並沒有根本性的解決在HuaWei手機上崩潰的問題,我通過在HuaWei手機上做測試發現根本原因是HuaWei自家定製的ROM系統中有一個白名單機制,只有加入了白名單的APP才允許註冊超過500個BroadcastReceiver,否則就會拋出Register too many Broadcast Receivers的異常。也就是說沒有加入該白名單機制的APP最多隻能註冊500個BroadcastReceiver,我是怎麼發現這個白名單機制的呢?在華爲手機上做如下測試,新建項目工程HuaWeiVerifier,MainActivity的佈局文件如下所示:
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context="com.llew.huawei.verifier.MainActivity">

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Register"
        android:onClick="register"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>
       MainActivity的佈局文件中僅僅添加了一個Button,當點擊Button的時候就會執行MainActivity的register()方法,MainActivity代碼如下所示:
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    public void register(View view) {
        for (int i = 1; i <= 1000; i++) {
            IntentFilter filter = new IntentFilter();
            filter.addAction("test index : " + i);
            registerReceiver(new BroadcastReceiver() {
                @Override
                public void onReceive(Context context, Intent intent) {
                }
            }, filter);
            Log.e(getClass().getName(), "當前註冊了:" + i + " 個廣播接收器");
        }
    }
}
       然後在HuaWei手機上運行該Demo,點擊Button後打印Log如下所示:

       從打印的日誌信息看,當註冊了500個BroadcastReceiver後,再繼續註冊BroadcastReceiver就會拋出異常,異常信息爲:registered 501 Receivers  in 1 Contexts,也就是說當前的應用通過Context註冊的BroadcastReceiver超過了500個。根據項目運行後拋出的異常信息,在HuaWei手機上一個應用只能註冊500個BroadcastReceiver,除非當前應用加入了白名單裏。

       既然HuaWei定製的ROM系統做了限制,那麼肯定是在哪一個類中做了校驗操作,根據crash信息可以知道崩潰是發生在LoadedApk類的checkRecevierRegisteredLeakLocked()方法中,由於我們沒法拿到HuaWei手機ROM系統中LoadedApk類的具體代碼,只能通過反射對比在HuaWei手機和Google Pixel手機上這倆LoadedAPK究竟有和不同,功夫不負有心人,最終發現在HuaWei的ROM的LoadedApk類中定義了一個叫mReceiverResource的成員變量,根據名稱我們猜測該屬性就是和註冊BroadcastReceiver的數量相關的類。在HuaWei手機中的Debug模式下,LoadedApk信息如下所示:


       mReceiverResource是ReceiverResource類型,ReceiverResource內部定義了一個ArrayList類型的成員變量mWhiteList,直接看名字就知道是白名單的意思,mWhiteList中默認添加了"com.tencent.mm"字符串,該字符串就是微信的包名。由此可知HuaWei定製的ROM系統把微信加入了白名單裏從而允許微信可以註冊超過500個BroadcastReceiver。爲了驗證mReceiverResource是用來控制註冊BroadcastReceiver的數量的,我把HuaWei手機上的微信卸載了,然後把剛剛創建的工程包名改爲com.tencent.mm,緊接着再運行工程,這時候果然可以註冊超過500個BroadcastReceiver了,打印日誌如下所示:


       從實驗結果來看,果真的如我們前邊猜測的那樣,mReceiverResource就是用來控制非白名單中的APP最多隻能註冊500個BraodcastReceiver的開關,只要把我們APP的packageName添加到白名單中,也就跳過了HuaWei手機的限制,頓時好開心呀,終於可以從根本上解決該問題了(*^__^*) ……

       接下來的工作就是通過反射來拿到LoadedApk中的mReceiverResource中的mWhiteList對象,然後把我們APP的packageName加入到mWhiteList中就行了。記得在前邊的文章中提過,反射技術在Java開發中是很重要的,學會了反射,你可以做很多事情……爲了方便小夥伴們使用反射,我抽出了一個精簡的反射庫Reflection,該庫目前已經開源到了Github上並上傳到了Jcenter倉庫中,使用的時候只需簡單的引入就行了:compile 'com.llew:reflect:1.0.1'

       下邊就是通過反射把我們APP的packageName添加到mWhiteList中的操作,實現起來並不複雜。由於我沒法在全部的HuaWei手機上做驗證測試,只是大概的測試了下,如果有小夥伴們能夠用手裏的HuaWei手機幫忙做下驗證和完善,不勝感激……LoadedApkHuaWei全部代碼如下所示:

public class LoadedApkHuaWei {


    public static void hookHuaWeiVerifier(Context baseContext) {
        try {
            if (null != baseContext && "ContextImpl".equals(baseContext.getClass().getSimpleName())) {
                IMPL.verifier(baseContext);
            } else {
                Log.w(LoadedApkHuaWei.class.getSimpleName(), "baseContext is't instance of ContextImpl");
            }
        } catch (Throwable ignored) {
            // ignore it
        }
    }


    private static final HuaWeiVerifier IMPL;

    static {
        final int version = android.os.Build.VERSION.SDK_INT;
        if (version >= 26) {
            IMPL = new V26VerifierImpl();
        } else if (version >= 24) {
            IMPL = new V24VerifierImpl();
        } else {
            IMPL = new BaseVerifierImpl();
        }
    }

    private static class V26VerifierImpl extends BaseVerifierImpl {

        private static final String WHITE_LIST = "mWhiteListMap";

        @Override
        public void verifier(Context baseContext) throws Throwable {
            Object whiteListMapObject = getWhiteListObject(baseContext, WHITE_LIST);
            if (whiteListMapObject instanceof Map) {
                Map whiteListMap = (Map) whiteListMapObject;
                List whiteList = (List) whiteListMap.get(0);
                if (null == whiteList) {
                    whiteList = new ArrayList<>();
                    whiteListMap.put(0, whiteList);
                }
                whiteList.add(baseContext.getPackageName());
            }
        }
    }

    private static class V24VerifierImpl extends BaseVerifierImpl {

        private static final String WHITE_LIST = "mWhiteList";

        @Override
        public void verifier(Context baseContext) throws Throwable {
            Object whiteListObject = getWhiteListObject(baseContext, WHITE_LIST);
            if (whiteListObject instanceof List) {
                List whiteList = (List) whiteListObject;
                whiteList.add(baseContext.getPackageName());
            }
        }
    }

    private static class BaseVerifierImpl implements HuaWeiVerifier {

        private static final String WHITE_LIST = "mWhiteList";

        @Override
        public void verifier(Context baseContext) throws Throwable {
            Object receiverResourceObject = getWhiteListObject(baseContext, WHITE_LIST);
            if (receiverResourceObject instanceof String[]) {
                String[] whiteList = (String[]) receiverResourceObject;
                List<String> newWhiteList = new ArrayList<>();
                newWhiteList.add(baseContext.getPackageName());
                Collections.addAll(newWhiteList, whiteList);
                FieldUtils.writeField(receiverResourceObject, WHITE_LIST, newWhiteList.toArray(new String[newWhiteList.size()]));
            }
        }

        Object getWhiteListObject(Context baseContext, String whiteList) throws Throwable {
            Field receiverResourceField = FieldUtils.getDeclaredField("android.app.LoadedApk", "mReceiverResource", true);
            if (null != receiverResourceField) {
                Field packageInfoField = FieldUtils.getDeclaredField("android.app.ContextImpl", "mPackageInfo", true);
                if (null != packageInfoField) {
                    Object packageInfoObject = FieldUtils.readField(packageInfoField, baseContext);
                    if (null != packageInfoObject) {
                        Object receivedResource = FieldUtils.readField(receiverResourceField, packageInfoObject, true);
                        if (null != receivedResource) {
                            return FieldUtils.readField(receivedResource, whiteList);
                        }
                    }
                }
            }
            return null;
        }
    }

    private interface HuaWeiVerifier {
        void verifier(Context baseContext) throws Throwable;
    }
}

       LoadedApkHuaWei只對外暴露一個hookHuaWeiVerifier()方法,該方法內部實現是根據HuaWei手機不同的版本做了不同的Hook操作。該API使用非常簡單,需要在自己APP中顯示的定義一個Application,然後在Application的onCreate()方法中調用LoadedApkHuaWei的hookHuaWeiVerifier()方法就行了,如下所示:

public class SimpleApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        LoadedApkHuaWei.hookHuaWeiVerifier(getBaseContext());
    }
}

       以上操作就可以把我們APP添加進HuaWei ROM中的白名單裏了(*^__^*) ……以後再也不怕只在HuaWei手機上出現Register too many Broadcast Receivers的Crash了。目前我把該庫起名爲HuaWeiVerifer並開源到了GitHub上也上傳了jcenter倉庫中,希望給遇見同樣問題的小夥伴們一點幫助……

       另外需要注意的是LoadedApkHuaWei的hookHuaWeiVerifier(Context baseContext)需要傳遞進去的是當前APP的baseContext,不要弄錯了。這個庫沒有做過全面的驗證,可能還有不兼容的情況,這裏歡迎小夥伴們fork and pr


       HuaWeiVerifer的GitHub地址:https://github.com/llew2011/HuaWeiVerifier,歡迎小夥伴們fork and star




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