转载请注明出处: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的布局文件如下所示:
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 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