背景说明:
Android有一个系统级的悬浮小窗,可以将我们的view,放入到系统级别的小窗中,然后就能在其他应用中开启这个悬浮窗,然后显示到我们的view,从而实现跨app展示view,这是一个很常见的的功能,在网上一搜一大把
在这里我贴一下比较关键的代码
windowManager = getWindowManager(context);
//需要悬浮的view
floatTouchView = new FloatingView(context);
//设置系统window的参数
floatParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
floatParams.gravity = Gravity.TOP | Gravity.LEFT;
floatParams.x = 600;
floatParams.y = 1000;
floatParams.flags = WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
//不加这一句,会出现悬浮按钮有黑边框
floatParams.format = PixelFormat.TRANSLUCENT;
floatParams.width = 600;
floatParams.height = 800;
//重点是在这里,在o版本后面就是需要用到TYPE_APPLICATION_OVERLAY这个参数,在o前面仅需要TYPE_SYSTEM_ALERT就可以了。
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
floatParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
else {
floatParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT;
}
}
floatTouchView.setLayoutParams(floatParams);
windowManager.addView(floatTouchView, floatParams);
很简单是吧,**注意:但是在O版本后面需要用到这个一个TYPE_APPLICATION_OVERLAY参数,当你使用到这一个参数的话就需要你去开启系统中开启该应用的悬浮窗权限才能正常使用。**所以一般我们会去判断一下你是否开启了这个权限,如果没有开启的话会跳转到系统权限页面,让用户开启这个权限。
现在我遇到一个客户,他们的产品设计说不希望跳转到权限开启页面,可以不跨App显示View,仅App内显示这个小窗。
一开始我的想法很简单,但殊不知这里面有着巨坑在等着我。犹如潜伏在深处的安康鱼,正等着放松警惕的我在上面游过,然后一口将我吞噬。
落入陷阱
一开是我的想法很简单,就是既然用户不想开启跳转到开启权限的系统页面,那么我就不跳转呗,也就是说我在没开启上面跳转权限的情况下我也用了TYPE_APPLICATION_OVERLAY这个参数,因为一开始我是不知道这个参数是需要开启悬浮权限的。
最为魔幻的地方来了,我居然通过了,我真的实现了在没有开启权限悬浮窗,实现了上面客户的要求,仅在app内的小窗悬浮,当你退出或进入其他app页面,这个小窗就不会显示,当你重新进入该应用就能显示这个小窗了。然后我美滋滋地将这个方案体交付给客户,心想不过而已,一段时间过去了,突然客户突然告诉我这个套方案不行,当他这样改动后会崩溃,Error提示没有权限,当时我就很纳闷,我这边是能通过的,但是他居然告诉我崩溃了,然后接下来的解决方案都是围绕着这一个问题进行的。这里我先告诉大家这里发生了什么事,这是一个巨坑,首先先说一下我用的是真机oppo ace android 9 ,我的系统也就是ColorOs ,然后客户用的测试机是android11 的 onePlus 11,问题就出在这里了,后面经过我的一系列的追踪发现,一顿源码追看,然后就发现了上面我说的那一个结论:O版本后面需要用到这个一个TYPE_APPLICATION_OVERLAY参数,当你使用到这一个参数的话就需要你去开启系统中开启该应用的悬浮窗权限才能正常使用,这一个就是客户会崩溃的原因,那么另一个问题是,我为什么可以成功?我大胆推测就是Oppo的ColorOs系统对于系统的悬浮窗权限源码肯定有过修改,不然按道理来讲我也会崩溃的,但是是不是这样我就考究不了了,因为我没有找到关于ColorOs这一块的系统源码。接下来我将会讲述我是怎么找到这个问题,并且我又是如何解决这个问题的。
修复问题
权限问题
重新回到我一脸懵逼的地方,为什么我可以,而客户不可以?看error报的是权限的问题,并且发现客户用的是android11 而我是android 9 那么是不是android11 在权限这一块有改动,ok那么我直接去看一下系统源码,看一下改动。首先报错提示的是android.view.ViewRootImpl$w@62ad156 -- permission denied for window type 2038
那么我们先去看一下这一块报错系统源码在哪里,如果对系统源码熟悉的话就很快定位到位置,如果不熟悉也没关系,直接百度你也能找到这一块地点,看多了你也能找到在哪里。那么提示错误的地方就是在这里
class ViewRootImpl
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
int userId) {
...
if (res < WindowManagerGlobal.ADD_OKAY){
...
switch (res) {
...
case WindowManagerGlobal.ADD_PERMISSION_DENIED:
throw new WindowManager.BadTokenException("Unable to add window "
+ mWindow + " -- permission denied for window type "
+ mWindowAttributes.type);
...
}
}
...
}
好的,我们找到错误的地方了,那么问题就出在这里额,res为WindowManagerGlobal.ADD_PERMISSION_DENIED,那么这个res在哪里取值的呢?往上查看,就在上面
res = mWindowSession.addToDisplayAsUser(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), userId, mTmpFrame,
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mDisplayCutout, inputChannel,
mTempInsets, mTempControls);
setFrame(mTmpFrame);
这里就要去看这个addToDisplayAsUser -> Session(但这只是一个跨进程的操作,获取到的实现类是WindowManagerService),调用的就是WindowManagerService.addWindow()(很多时候我们点进去看源码都会看到Session这种跨进程操作,但是并不知道具体的实现类是那一个类来做的,通常我的做法是直接百度搜这个Session,网上有很多分析Android系统源码帖子,我们就很容易找到那一个是具体的是实现类,那么我们再去看这个实现类的源码就可以了)
然后错误就在
class WindowManagerService.addWindow()
int res = mPolicy.checkAddPermission(attrs.type, isRoundedCornerOverlay, attrs.packageName,
appOp);
if (res != WindowManagerGlobal.ADD_OKAY) {
return res;
}
那么问题就在这个mPolicy.checkAddPermission了,这个mPolicy的具体是实现对象是PhoneWindowManager.checkAddPermission()
这里的代码很短我粘贴出来看一下
class PhoneWindowManager
public int checkAddPermission(int type, boolean isRoundedCornerOverlay, String packageName,
int[] outAppOp) {
if (isRoundedCornerOverlay && mContext.checkCallingOrSelfPermission(INTERNAL_SYSTEM_WINDOW)
!= PERMISSION_GRANTED) {
return ADD_PERMISSION_DENIED;
}
outAppOp[0] = AppOpsManager.OP_NONE;
if (!((type >= FIRST_APPLICATION_WINDOW && type <= LAST_APPLICATION_WINDOW)
|| (type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW)
|| (type >= FIRST_SYSTEM_WINDOW && type <= LAST_SYSTEM_WINDOW))) {
return WindowManagerGlobal.ADD_INVALID_TYPE;
}
if (type < FIRST_SYSTEM_WINDOW || type > LAST_SYSTEM_WINDOW) {
// Window manager will make sure these are okay.
return ADD_OKAY;
}
if (!isSystemAlertWindowType(type)) {
switch (type) {
case TYPE_TOAST:
// Only apps that target older than O SDK can add window without a token, after
// that we require a token so apps cannot add toasts directly as the token is
// added by the notification system.
// Window manager does the checking for this.
outAppOp[0] = OP_TOAST_WINDOW;
return ADD_OKAY;
case TYPE_INPUT_METHOD:
case TYPE_WALLPAPER:
case TYPE_PRESENTATION:
case TYPE_PRIVATE_PRESENTATION:
case TYPE_VOICE_INTERACTION:
case TYPE_ACCESSIBILITY_OVERLAY:
case TYPE_QS_DIALOG:
case TYPE_NAVIGATION_BAR_PANEL:
// The window manager will check these.
return ADD_OKAY;
}
return (mContext.checkCallingOrSelfPermission(INTERNAL_SYSTEM_WINDOW)
== PERMISSION_GRANTED) ? ADD_OKAY : ADD_PERMISSION_DENIED;
}
// Things get a little more interesting for alert windows...
outAppOp[0] = OP_SYSTEM_ALERT_WINDOW;
final int callingUid = Binder.getCallingUid();
// system processes will be automatically granted privilege to draw
if (UserHandle.getAppId(callingUid) == Process.SYSTEM_UID) {
return ADD_OKAY;
}
ApplicationInfo appInfo;
try {
appInfo = mPackageManager.getApplicationInfoAsUser(
packageName,
0 /* flags */,
UserHandle.getUserId(callingUid));
} catch (PackageManager.NameNotFoundException e) {
appInfo = null;
}
if (appInfo == null || (type != TYPE_APPLICATION_OVERLAY && appInfo.targetSdkVersion >= O)) {
/**
* Apps targeting >= {@link Build.VERSION_CODES#O} are required to hold
* {@link android.Manifest.permission#INTERNAL_SYSTEM_WINDOW} (system signature apps)
* permission to add alert windows that aren't
* {@link android.view.WindowManager.LayoutParams#TYPE_APPLICATION_OVERLAY}.
*/
return (mContext.checkCallingOrSelfPermission(INTERNAL_SYSTEM_WINDOW)
== PERMISSION_GRANTED) ? ADD_OKAY : ADD_PERMISSION_DENIED;
}
// check if user has enabled this operation. SecurityException will be thrown if this app
// has not been allowed by the user. The reason to use "noteOp" (instead of checkOp) is to
// make sure the usage is logged.
final int mode = mAppOpsManager.noteOpNoThrow(outAppOp[0], callingUid, packageName,
null /* featureId */, "check-add");
switch (mode) {
case AppOpsManager.MODE_ALLOWED:
case AppOpsManager.MODE_IGNORED:
// although we return ADD_OKAY for MODE_IGNORED, the added window will
// actually be hidden in WindowManagerService
return ADD_OKAY;
case AppOpsManager.MODE_ERRORED:
// Don't crash legacy apps
if (appInfo.targetSdkVersion < M) {
return ADD_OKAY;
}
return ADD_PERMISSION_DENIED;
default:
// in the default mode, we will make a decision here based on
// checkCallingPermission()
return (mContext.checkCallingOrSelfPermission(SYSTEM_ALERT_WINDOW)
== PERMISSION_GRANTED) ? ADD_OKAY : ADD_PERMISSION_DENIED;
}
}
这里是整体的代码,我标记一下重点的地方,一进来我们就看到我们想看到的代码了,就是下面的这一段会返回ADD_PERMISSION_DENIED,那么我们进去看一下里面的跳转就是这样contextImpl.checkCallingOrSelfPermission() -> PermissionManager.checkAddPermission(),具体我就不看了,大概就是检查一下有没有开启权限什么的。
if (isRoundedCornerOverlay && mContext.checkCallingOrSelfPermission(INTERNAL_SYSTEM_WINDOW)
!= PERMISSION_GRANTED) {
return ADD_PERMISSION_DENIED;
}
注意这里我看的是Android11的系统源码,然后我看一下Android9 的这一块是怎么样的
public int checkAddPermission(WindowManager.LayoutParams attrs, int[] outAppOp) {
final int type = attrs.type;
final boolean isRoundedCornerOverlay =
(attrs.privateFlags & PRIVATE_FLAG_IS_ROUNDED_CORNERS_OVERLAY) != 0;
if (isRoundedCornerOverlay && mContext.checkCallingOrSelfPermission(INTERNAL_SYSTEM_WINDOW)
!= PERMISSION_GRANTED) {
return ADD_PERMISSION_DENIED;
}
卧槽,也是这一个样子,这个时候我是一阵心惊,那么岂不是Android9 也不能通过,因为我是没有去开启悬浮权限的,我立刻开启虚拟机测试一番,果然报同样的错误,这个时候我就已经推测出原生的Android系统是不能通过这种方式来进行小窗悬浮的,而我能通过的原因就应该是ColorOs对这一部分的权限进行修改,从而导致了我能通过的原因。
根据下面这篇官方文档,我们知道了当你使用TYPE_APPLICATION_OVERLAY是需要配合着SYSTEM_ALERT_WINDOW权限一起使用的
https://developer.android.com/reference/android/view/WindowManager.LayoutParams#TYPE_APPLICATION_OVERLAY
好,我们现在已经找到了为什么客户 不行,而我们可以的问题了,现在我们需要的是实现的是非系统级的小窗,那么现在是不想开启系统权限而能实现小窗。
我们通过查看上面的源码知道只有 return ADD_OKAY就能通过这个判断了,那么我们可以看到这一句
if (type < FIRST_SYSTEM_WINDOW || type > LAST_SYSTEM_WINDOW) {
// Window manager will make sure these are okay.
return ADD_OKAY;
}
而TYPE_APPLICATION_OVERLAY是2038,
FIRST_SYSTEM_WINDOW = 2000
LAST_SYSTEM_WINDOW = 2999
那如果我们的type是小于2000或是大于2999就能直接返回ADD_OKAY了,这里的Type就是我们给Window设置的属性,就是这一段
floatParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
据我所知window分成3中类型
APPLICATION_WINDOW: 1-99
SUB_WINDOW: 1000-1999
SYSTEM_WINDOW: 2000-2999
那么这里我根据上面的官方文档,我尝试着给他赋值TYPE_APPLICATION,也就是
floatParams.type = WindowManager.LayoutParams.TYPE_APPLICATION;
注意根据文档上面的提示,我们创建这个window的token必须是Activity的,也就是说必须 windowManager = getWindowManager(context);中的context必须是Activity的
然后运行一下,芜湖通过了,不崩溃了。
Token问题
但是事情没有那么简单,后面我点击小窗,这个时候跳转到小窗的画面了,然后退出去,再进来开启小窗,这个时候崩溃了,报的错误信息是:
android.view.WindowManager$BadTokenException: Unable to add window -- token android.os.BinderProxy@b5c68b4 is not valid; is your activity running?
at android.view.ViewRootImpl.setView(ViewRootImpl.java:798)
at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:356)
at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:93)
at
在这里我先说明一下,我的这个小窗的场景,首先我们在MainActivity中创建一个RelationLayout,这个Layout是布满全屏,然后为这个Activity创建一个系统的小窗口(Window),然后我们将一个View放入到这个小窗,当点击这个小窗的时候需要将这个View放大,那么就将这个view从Window中移除,然后将这个View放入到Layout中。
现在为什么会报这个错误?同样的处理方法,先去看一下这个错误的源码是在那里。
就在ViewRootImpl.setView中
class ViewRootImpl
setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
int userId){
...
switch (res) {
if (res < WindowManagerGlobal.ADD_OKAY) {
case WindowManagerGlobal.ADD_BAD_APP_TOKEN:
case WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN:
throw new WindowManager.BadTokenException(
"Unable to add window -- token " + attrs.token
+ " is not valid; is your activity running?");
...
}
当第二次开启小窗的时候,报的错误就在这里,还是这个res的问题,在上面我们知道这个res的赋值是这一句
res = mWindowSession.addToDisplayAsUser(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(), userId, mTmpFrame,
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mDisplayCutout, inputChannel,
mTempInsets, mTempControls);
那么走的还是这个调用的就是WindowManagerService.addWindow(),并且这个res=ADD_BAD_SUBWINDOW_TOKEN 或是 res = ADD_BAD_APP_TOKEN的时候才会报这个错误,那么我们看一下在WindowManagerService.addWindow()中什么时候return 了 ADD_BAD_SUBWINDOW_TOKEN或是ADD_BAD_APP_TOKEN
先看一下ADD_BAD_SUBWINDOW_TOKEN
class WindowManagerService
void addWindow(){
if (type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW) {
parentWindow = windowForClientLocked(null, attrs.token, false);
if (parentWindow == null) {
ProtoLog.w(WM_ERROR, "Attempted to add window with token that is not a window: "
+ "%s. Aborting.", attrs.token);
return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN;
}
if (parentWindow.mAttrs.type >= FIRST_SUB_WINDOW
&& parentWindow.mAttrs.type <= LAST_SUB_WINDOW) {
ProtoLog.w(WM_ERROR, "Attempted to add window with token that is a sub-window: "
+ "%s. Aborting.", attrs.token);
return WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN;
}
}
}
全局搜这个ADD_BAD_SUBWINDOW_TOKEN很容易发现就两个地方有,就是上面了,但是这里的进入条件是type >= FIRST_SUB_WINDOW && type <= LAST_SUB_WINDOW,很显然不是这个ADD_BAD_SUBWINDOW_TOKEN,那么就是ADD_BAD_APP_TOKEN了。那就搜一下这个ADD_BAD_APP_TOKEN吧,好家伙一搜有7个地方返回这个东西。那一个才是真正返回的呢?我是不知道如何直接调试系统源码的(如果有懂得的哥们可以在下面评论告诉我一下),这里只能一个一个看了。其实也不是很多
class WindowManagerService
void addWindow(){
...
if (token == null) {
if (!unprivilegedAppCanCreateTokenWith(parentWindow, callingUid, type,
rootType, attrs.token, attrs.packageName)) {
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
final IBinder binder = attrs.token != null ? attrs.token : client.asBinder();
token = new WindowToken(this, binder, type, false, displayContent,
session.mCanAddInternalSystemWindow, isRoundedCornerOverlay);
}
...
else if (rootType == TYPE_INPUT_METHOD) {
if (token.windowType != TYPE_INPUT_METHOD) {
ProtoLog.w(WM_ERROR, "Attempted to add input method window with bad token "
+ "%s. Aborting.", attrs.token);
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
} else if (rootType == TYPE_VOICE_INTERACTION) {
if (token.windowType != TYPE_VOICE_INTERACTION) {
ProtoLog.w(WM_ERROR, "Attempted to add voice interaction window with bad token "
+ "%s. Aborting.", attrs.token);
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
} else if (rootType == TYPE_WALLPAPER) {
if (token.windowType != TYPE_WALLPAPER) {
ProtoLog.w(WM_ERROR, "Attempted to add wallpaper window with bad token "
+ "%s. Aborting.", attrs.token);
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
} else if (rootType == TYPE_ACCESSIBILITY_OVERLAY) {
if (token.windowType != TYPE_ACCESSIBILITY_OVERLAY) {
ProtoLog.w(WM_ERROR,
"Attempted to add Accessibility overlay window with bad token "
+ "%s. Aborting.", attrs.token);
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
} else if (type == TYPE_TOAST) {
// Apps targeting SDK above N MR1 cannot arbitrary add toast windows.
addToastWindowRequiresToken = doesAddToastWindowRequireToken(attrs.packageName,
callingUid, parentWindow);
if (addToastWindowRequiresToken && token.windowType != TYPE_TOAST) {
ProtoLog.w(WM_ERROR, "Attempted to add a toast window with bad token "
+ "%s. Aborting.", attrs.token);
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
} else if (type == TYPE_QS_DIALOG) {
if (token.windowType != TYPE_QS_DIALOG) {
ProtoLog.w(WM_ERROR, "Attempted to add QS dialog window with bad token "
+ "%s. Aborting.", attrs.token);
return WindowManagerGlobal.ADD_BAD_APP_TOKEN;
}
...
}
这里我只粘贴出返回我们需要的ADD_BAD_APP_TOKEN的地方,仔细看一下,其实这个ADD_BAD_APP_TOKEN是和type和rootType有关的,这个type就是我们给窗口的属性type,在这里就是TYPE_APPLICATION,那么下面跟type有关的就排除掉了,还有一个就是rootType了,rootType有关的地方是在这里
final int rootType = hasParent ? parentWindow.mAttrs.type : type;
这里我就直接说这个hasParenty在这里就是false的,详细过程我就先不说了,你看了这一块源码的话就是知道这里肯定是false的。那么的话返回的ADD_BAD_APP_TOKEN的地方就是token为null的才会返回。我们看一下这个token在哪里赋值的,就在下面的这一行代码。
WindowToken token = displayContent.getWindowToken(
hasParent ? parentWindow.mAttrs.token : attrs.token);
hasParent为null,那么这里就是attrs.token,也就是窗口的属性token。这里我就产生了一个疑问,那就是为什么第一次进来的时候token不为null,而第二次这个token 就是null的?这个token是通过displayContent.getWindowToken(attrs.token)来获取到的,那么我们看一下这个方法,首先这个displayContent是DisplayContent类行,直接看一下这个类里面的getWindowToken()方法
class DisplayContent
WindowToken getWindowToken(IBinder binder) {
return mTokenMap.get(binder);
}
他就是从这个mTokenMap中获取的,这里是在map中通过binder作为key来找到对应的token值,那么我的疑问就是为什么第二次进来的token是null,难道被清掉了吗?那么是在什么地方添加值的呢?
class DisplayContent
void addWindowToken(IBinder binder, WindowToken token) {
...
mTokenMap.put(binder, token);
...
}
那么在这里这个attrs.token就是这个binder了,这个attrs就是我们给Window设置的属性值,那么我们尝试给这个window添加一个token参数吧,
floatParams.token = ((Activity)context).getWindow().getAttributes().token;
直接给Activity的token给这个window,然后再复现一下上面的操作,这个时候发现还是有同样的错误,奇怪,然后我通过debug来看一下这个窗口的window和Activity的token,输出结果看一下
Activity 11695
smallwindow 11695
第二次
Activity 12294
smallwindow 11695
可以发现问题的所在了,第一次的Activity和window的token一致,但是第二次的window和第一次的Activity一直和第二次的Activity不一致,就是说token不变,这个时候我重新看了一下我写的代码,我把这个window的属性设置成了static了
private static WindowManager.LayoutParams floatParams;
汗~~
既然问题找到了,修复就很简单了,销毁window的时候将floatParams 设置为null,然后在重新添加新的窗口重新赋值。重新执行一次完美通过。
窗体泄漏问题
还有一个问题,就是窗体泄漏的问题,一开始我在onDestroy中将窗体的window的和view都销毁掉,但是这边当Activity销毁后依然出现上述的问题,窗体泄漏,这个时候百思不得其解,通过profiler的heap dump进行内存分析也没什么发现,因为是在onDestroy的时候才发生,这个时候已经发生错误了,根本获取不到,然后再仔细分析这个错误日志,发现是flowerView导致的窗体泄漏,就flowerView没有被销毁掉,按道理讲flowerView应该被销毁掉的才对,一顿搜索后,发现关于这个窗体泄漏最多的就是这个dialog了,然后我看了下代码,我将这个flowerView的Params.type设置成了WindowManager.LayoutParams.TYPE_APPLICATION其实已经相当于一个应用的窗口了,那么是不是不能通过正常的手段进行销毁呢?这个时候我看了一下Dialog的dimiss()方法,看一下里面的销毁时怎样的,果然不一样,里面的销毁时这样的
最为关键的两句
mWindowManager.removeViewImmediate(mDecor);
mWindow.closeAllPanels();
然后通过这两句话就能正常地销毁掉这个浮窗view了。
总结
虽然这一次的排除问题并没有太多有营养的东西,但是重点还是在于我排除问题的手段,这里我是记录一下我是如何去debug的,如何去排除问题的过程,很多时候我们排除问题都是通过百度、Google,我也是这么做的,毕竟这是最高效的方法,但往往我们找到答案仅是知其然而不知其所以然,甚至会找不到和我们问题相符的帖子,这个时候我们能做的是根据Error日志来去系统源码去找,只有源码是不会骗你的。在这里我就是倒在了对系统源码WMS这一块地方的,就是这一块地方不熟悉导致我浪费了大量的时间来排查错误。
源码阅读技巧
在这里我顺便分析下看源码的小技巧,源码的阅读对于我们的重要性不用我多说了,无论是系统源码或是第三方开源库的源码,阅读其中定让你增进不少,不仅是方便你解决问题,也会让你偷学到更多代码的设计方式。
源码很多,其中有很大的一部分你是完全看不懂的,但是看不懂无所谓,因为我们看源码是带着目的去看的,所以这个时候我们优先去看那一些我们看得懂的地方先,假如我们需要看的代码就在这些我们看不懂的地方的话,那只能慢慢看了,但可以先将那一些我们看不懂的代码放到百度上搜一下,通常网上有很多人分析过Android源码的,这样你可以看着他们的帖子一步一步地去分析,这样可以轻松很多。
然后我记得在康师傅在某一期的教学视频中也谈过阅读源码的技巧,其中我们要善用搜索方法。我看源码的时候最经常需要看某个引用是在哪里被创建或是哪里被赋值的,那么我们可以搜 "xxx = " 这样可以很快搜索出来。如果是map的话,可以直接搜索“xxx.put(”
另外一点就是在Android Studio中阅读Android源码很多时候,当你进去后你就会发现很多类很多方法你都是点不进去的,这个时候就需要到网上的一些收录Android源码的网站上去查看对应的类或方法。
这里我推荐http://aospxref.com/这个网址,现在已经收录了Android11 和Android12了,不仅是java层的源码,还包含了native层的C代码,简直就是给力。
还有一种就是遇到Session的这一种情况,这种就是跨进程操作的代码,想要找到他的具体实现类很麻烦(其实我也不知道具体实现类是那一个)这个时候就是需要你去网上找别人分析这一块的帖子,通常都会粘贴出来的。
这段时间的个人总结
下面的这一部份是我这段时间的一些总结,和这篇帖子没有太大的关系,没有兴趣的老哥可以不看。首先我上一篇帖子说我正在准备整一个大活,然而这一篇并不是我说的大活,这一篇问题排查的帖子按时间线来讲应该是过年后应该就出来的,但是因为一些原因只能现在挤出点时间写出来,下面就是说一下这一个原因,也算是我求助一下各位阳光沙滩的老哥的帮助。
首先是我准备的大活真的进展缓慢,最重要还是因为我接下来所说的原因。
我这边其实算是一个刚入行的新人吧,也是跟着康师傅学习了不少的知识,在这里真的衷心的说一句:感谢阳光沙滩让你我在此相遇。我这边是在一间ToB的公司工作,作为新人负责的就是提供客户的技术支持,所以最近的一两篇文章也是关于客户遇到的一些非常奇怪的事情。所以我会经常遇到一些我们日常开发中所遇不到的Case,这些客户的问题千奇百怪。所以我入职的这段时间真的学到了不少的东西,最重要的是加班并不是很严重,到点就能走了。但是在过年前公司需要搞一些和Ue4(虚幻引擎4)有关的项目,因为这个项目非常缺人,所以从很多部门都抽调了不少的同事过来搞这个Ue4的项目,我也不例外被抽调过来了,作为一个完全没有接触过Ue4的人来讲,一切都是重头开始学,搞这个项目我也是边学边做的,但是我感觉搞这个Ue4 的项目有点遥遥无期了,感觉还需要做很久。其实我内心并不是很排斥学习这个Ue4的,毕竟谁小的时候没想过做一个属于自己的游戏呢?但如果是放在两年后的我来做(这个时候我感觉我应该把Android的基础学的差不多了,能稳吃Android这碗饭了,可以去尝试做其他的事情了),我是完全可以接受的,但是如果放到现在来做的话,这和我的个人目标和个人的规划有着很大的冲突,因为现在Ue4项目太忙了,我每天都在忙一些关于Ue4东西,对于Android的知识学习进度太慢了,我都是抽一些时间来学的,其中受影响最大的就是我在准备的大活,真的太慢了,并且Android是我目前喜欢做,并且是用来吃饭的工具。(幸运的是Ue4的脚步是用c++来写的,这对我的NDK开发有着很大的帮助)有一句话说的很有道理,合适的时间做合适的事情,我感觉我当前应该全心身是投入到Android的事业当中去。另外一个就是个人的成功是需要个人的奋斗加上历史进程的,我感觉当前的历史进程和我个人的奋斗开始有点背道而驰了,所以现在我有点迷茫了,我有点不知该怎么做了,我不知道该用一个什么样的心态去面对我现在所做的事情,希望各位老哥能给一点意见给我。换句话来讲其实程序员的职业周期很短,我担心我的进度慢了,担心我日后迈不过那一道坎。迈过了的话就能将职业生涯多延长几年(怎么感觉像在修仙?不能突破就会因元寿枯竭而亡)