Android专栏-JavaCrash默认处理

Java Crash 默认处理

CrashHandler 处理 Java 异常流程:

  • 区分Debug模式和Release模式、主进程和子进程、主线程和子线程来处理
  • 捕获Activity的生命周期内异常,并主动杀死Activity
  • View绘制流程异常捕获
  • 自定义上报

CrashHandler 源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
import android.os.Build
import android.os.Environment
import android.os.Handler
import android.os.Looper
import android.text.TextUtils
import android.util.Log
import com.alibaba.ha.adapter.AliHaAdapter
import com.rrtv.action.manager.ThreadPoolManager
import com.rrtv.utils.utils.UiUtils
import java.io.ByteArrayInputStream
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.lang.reflect.Field
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*


/**
* Java Crash 默认处理
*/
class CrashHandler : Thread.UncaughtExceptionHandler {

private var isDebug = false
private var isMainProcess = false

private var sActivityKiller: IActivityKiller? = null

companion object {
private const val TAG = "CrashHandler"
private var instance = CrashHandler()

@Volatile
@JvmStatic
private var hasInit = false


/**
* 初始化
*
* 在 @link Application#onCreate() 方法中 install()
*
* @param isDebug 是否debug模式 BuildConfig.DEBUG
* @param isMainProcess 是否是主进程 android.os.Process.myPid()
*/
@JvmStatic
fun install(isDebug: Boolean, isMainProcess: Boolean) {
if (!hasInit) {
synchronized(CrashHandler::class.java) {
if (!hasInit) {
Log.d(TAG, "install: isDebug = $isDebug, mainPid = $isMainProcess")
hasInit = true
instance.setup(isDebug, isMainProcess)
Log.d(TAG, "install success.")
}
}
}
}
}


private fun setup(isDebug: Boolean, isMainProcess: Boolean) {
Log.d(TAG, "setup:")
this.isDebug = isDebug
this.isMainProcess = isMainProcess

Thread.setDefaultUncaughtExceptionHandler(this)

if (!isDebug && isMainProcess) {
// release 模式防止主线程奔溃
Handler(Looper.getMainLooper()).post {
while (true) {
try {
Looper.loop()
} catch (e: Exception) {
handleLooperException(e)
}
}
}

try {
// 生命周期的 ActivityKiller
initActivityKiller()
} catch (e: Exception) {
Log.e(TAG, "拦截生命周期失败", e)
}
}
}

/**
* 替换ActivityThread.mH.mCallback,实现拦截Activity生命周期,直接忽略生命周期的异常的话会导致黑屏,目前
* 会调用ActivityManager的finishActivity结束掉生命周期抛出异常的Activity
*/
private fun initActivityKiller() {
Log.d(TAG, "initActivityKiller: Build.VERSION.SDK_INT=${Build.VERSION.SDK_INT}")
//各版本android的ActivityManager获取方式,finishActivity的参数,token(binder对象)的获取不一样
if (Build.VERSION.SDK_INT >= 28) {
sActivityKiller = ActivityKillerV28()
} else if (Build.VERSION.SDK_INT >= 26) {
sActivityKiller = ActivityKillerV26()
} else if (Build.VERSION.SDK_INT == 25 || Build.VERSION.SDK_INT == 24) {
sActivityKiller = ActivityKillerV24_V25()
} else if (Build.VERSION.SDK_INT in 21..23) {
sActivityKiller = ActivityKillerV21_V23()
} else if (Build.VERSION.SDK_INT in 15..20) {
sActivityKiller = ActivityKillerV15_V20()
} else if (Build.VERSION.SDK_INT < 15) {
sActivityKiller = ActivityKillerV15_V20()
}
try {
hookMH()
Log.e(TAG, "hookMH Success.")
} catch (e: Throwable) {
Log.e(TAG, "hookMH 失败: ", e)
}
}

@Throws(Exception::class)
private fun hookMH() {
Log.d(TAG, "hookMH: ")
val LAUNCH_ACTIVITY = 100
val PAUSE_ACTIVITY = 101
val PAUSE_ACTIVITY_FINISHING = 102
val STOP_ACTIVITY_HIDE = 104
val RESUME_ACTIVITY = 107
val DESTROY_ACTIVITY = 109
val NEW_INTENT = 112
val RELAUNCH_ACTIVITY = 126
val activityThreadClass =
Class.forName("android.app.ActivityThread")
val activityThread =
activityThreadClass.getDeclaredMethod("currentActivityThread").invoke(null)
val mhField: Field = activityThreadClass.getDeclaredField("mH")
mhField.setAccessible(true)
val mhHandler = mhField.get(activityThread) as Handler
val callbackField: Field = Handler::class.java.getDeclaredField("mCallback")
callbackField.setAccessible(true)
callbackField.set(mhHandler, Handler.Callback { msg ->
if (Build.VERSION.SDK_INT >= 28) { //android P 生命周期全部走这
val EXECUTE_TRANSACTION = 159
if (msg.what === EXECUTE_TRANSACTION) {
try {
mhHandler.handleMessage(msg)
} catch (throwable: Throwable) {
sActivityKiller?.finishLaunchActivity(msg)
handleLifecycleException(throwable)
}
return@Callback true
}
return@Callback false
}
when (msg.what) {
LAUNCH_ACTIVITY -> {
try {
mhHandler.handleMessage(msg)
} catch (throwable: Throwable) {
sActivityKiller?.finishLaunchActivity(msg)
handleLifecycleException(throwable)
}
return@Callback true
}
RESUME_ACTIVITY -> {
try {
mhHandler.handleMessage(msg)
} catch (throwable: Throwable) {
sActivityKiller?.finishResumeActivity(msg)
handleLifecycleException(throwable)
}
return@Callback true
}
PAUSE_ACTIVITY_FINISHING -> {
try {
mhHandler.handleMessage(msg)
} catch (throwable: Throwable) {
sActivityKiller?.finishPauseActivity(msg)
handleLifecycleException(throwable)
}
return@Callback true
}
PAUSE_ACTIVITY -> {
try {
mhHandler.handleMessage(msg)
} catch (throwable: Throwable) {
sActivityKiller?.finishPauseActivity(msg)
handleLifecycleException(throwable)
}
return@Callback true
}
STOP_ACTIVITY_HIDE -> {
try {
mhHandler.handleMessage(msg)
} catch (throwable: Throwable) {
sActivityKiller?.finishStopActivity(msg)
handleLifecycleException(throwable)
}
return@Callback true
}
DESTROY_ACTIVITY -> {
try {
mhHandler.handleMessage(msg)
} catch (throwable: Throwable) {
handleLifecycleException(throwable)
}
return@Callback true
}
}
false
})
}

/**
* 生命周期异常处理
*/
private fun handleLifecycleException(e: Throwable) {
Log.e(TAG, "lifecycleException: ", e)
reportCustomThrowable(Thread.currentThread(), RuntimeException("Activity生命周期出现异常", e))
// 给个Toast提示
try {
UiUtils.showToastSafe("小猿刚刚捕获了一只BUG")
} catch (e: Exception) {
e.printStackTrace()
}
}

/**
* 主线程Looper异常
*/
private fun handleLooperException(e: Exception) {
// 主线程内发生异常 主动 catch
Log.e(TAG, "handleLooperException: ", e)
reportCustomThrowable(Thread.currentThread(), e)
handleMainThreadException(e)
}


/**
* 本版本不处理
*
* view measure layout draw时抛出异常会导致Choreographer挂掉
* 建议直接杀死app。以后的版本会只关闭黑屏的Activity
*
* @param e
*/
private fun isChoreographerException(e: Throwable?): Boolean {
val elements = e?.stackTrace ?: return false
for (i in elements.size - 1 downTo -1 + 1) {
if (elements.size - i > 20) {
return false
}
val element = elements[i]
if ("android.view.Choreographer" == element.className && "Choreographer.java" == element.fileName && "doFrame" == element.methodName) {
//View 绘制流程出的问题
return true
}
}
return false
}


override fun uncaughtException(t: Thread, e: Throwable) {
Log.e(TAG, "uncaughtException: ")
if (isDebug) {
handleDebugException(t, e)
} else {
handleReleaseException(t, e)
}
}

/**
* 处理 Debug 模式的异常
*/
private fun handleDebugException(t: Thread, e: Throwable) {
Log.e(TAG, "handleDebugException: $t", e)
// 记录本地日志
saveThrowableMessage(Log.getStackTraceString(e))
}

/**
* 处理 !Debug 模式的异常
*/
private fun handleReleaseException(t: Thread, e: Throwable) {
Log.e(TAG, "handleReleaseException: $t", e)
// 自定义 Bug 上报
reportCustomThrowable(t, e)
// 根据情况来处理异常
if (isMainProcess) {
// 为主进程
if (Looper.myLooper() == Looper.getMainLooper()) {
// 主线程异常处理
handleMainThreadException(e)
} else {
// 非主线程
Log.e(TAG, "子线程异常: finish.")
}
} else {
// 如果是子进程发生异常 直接殺掉子進程
Log.e(TAG, "子进程异常: killProcess.")
android.os.Process.killProcess(android.os.Process.myPid())
}

}

/**
* 主线程异常处理
*
* Looper.loop & MainThreadUncaughtException
*/
private fun handleMainThreadException(e: Throwable) {
Log.e(TAG, "handleMainThreadException: ")
try {
// 主线程
when (e) {
is IllegalArgumentException,
is IllegalStateException,
is IndexOutOfBoundsException,
is UnsupportedOperationException,
is ArithmeticException,
is NumberFormatException,
is NullPointerException,
is ClassCastException,
is AssertionError,
is NoSuchElementException -> {
Log.e(TAG, "主线程异常: handle finish.")
if (isChoreographerException(e)) {
UiUtils.showToastSafe("界面刷新出了个小问题")
}
}
else -> {
Log.e(TAG, "主线程未知异常:System exit.")
// 这里也可以只给个提示,反正不会程序不会奔溃
android.os.Process.killProcess(android.os.Process.myPid())
System.exit(0)
}
}
} catch (e: Exception) {
e.printStackTrace()
}

}

private val logFilePath = Environment.getExternalStorageDirectory().toString() +
File.separator + "Example" +
File.separator + "CrashLog"

private fun saveThrowableMessage(errorMessage: String) {
if (TextUtils.isEmpty(errorMessage)) {
return
}
val file = File(logFilePath)
if (file.exists() && file.isDirectory) {
// fall through
} else {
file.mkdirs()
}
writeToFile(errorMessage, file)
}

private val formatter: DateFormat = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.CHINA)

private fun writeToFile(errorMessage: String, file: File) {
ThreadPoolManager.getShortPool()?.execute {
var outputStream: FileOutputStream? = null
try {
val timestamp = System.currentTimeMillis()
val time = formatter.format(Date())
val fileName = "crash-$time-$timestamp"
val inputStream = ByteArrayInputStream(errorMessage.toByteArray())
outputStream = FileOutputStream(File(file, "$fileName.txt"))
var len: Int
val bytes = ByteArray(1024)
while (inputStream.read(bytes).also { len = it } != -1) {
outputStream.write(bytes, 0, len)
}
outputStream.flush()
Log.e(TAG, "异常奔溃日志成功写入本地文件:${file.absolutePath}")
} catch (e: Exception) {
Log.e(TAG, "异常奔溃日志写入本地文件失败: ", e)
} finally {
if (outputStream != null) {
try {
outputStream.close()
} catch (e: IOException) {
// nothing
}
}
}
}
}

/**
* 自定义 Bug 上报
*/
private fun reportCustomThrowable(t: Thread, e: Throwable) {
Log.e(TAG, "reportCustomThrowable: ")
try {
AliHaAdapter.getInstance().reportCustomError(e) //配置项:自定义错误
} catch (ex: Exception) {
Log.e(TAG, "上报自定义异常Error: ", ex)
}
}
}

使用方式:

ApplicationonCreate() 方法中, 调用 RrCrashHandler.install(BuildConfig.DEBUG, AppUtils.isMainProgress(this))

参考链接:

https://github.com/android-notes/Cockroach

https://github.com/android-notes/Cockroach/blob/master/%E5%8E%9F%E7%90%86%E5%88%86%E6%9E%90.md

Android专栏-SmartRefreshLayout

Android专栏-SmartRefreshLayout

0x01 加载结束之后底部多出一段空白位置

SmartRefreshLayout 嵌套 ViewPager2 上拉加载更多,在 finishLoadMore() 方法之后,底部加载 Loading 位置会多出一段空白不消失。

解决方案:

1
smartRefreshLayout.setEnableScrollContentWhenLoaded(false)

0x02 下拉刷新+PAG动画

自定义下拉刷新头部,使用 PAGView 做动画,可以在 onMoving(boolean b, float v, int i, int i1, int i2) 方法中设置 pagView.setProgress(v); 添加手势动画。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.RelativeLayout;
import android.widget.TextView;

import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;

import com.scwang.smartrefresh.layout.api.RefreshHeader;
import com.scwang.smartrefresh.layout.api.RefreshKernel;
import com.scwang.smartrefresh.layout.api.RefreshLayout;
import com.scwang.smartrefresh.layout.constant.RefreshState;
import com.scwang.smartrefresh.layout.constant.SpinnerStyle;

import org.libpag.PAGFile;
import org.libpag.PAGView;

public class CommonRefreshHeader extends RelativeLayout implements RefreshHeader {
public static final String DEFAULT_LOADING_FILE = "load_bubble.pag";

protected View mView;
protected ImageView sdv_background;
private int mFinishDuration = 300;
private ViewGroup rootLayout;
private PAGView pagView;
private TextView toastTv;

public CommonRefreshHeader(Context context) {
super(context);
initView(context);
}

public CommonRefreshHeader(Context context, AttributeSet attrs) {
super(context, attrs);
initView(context);
}

public CommonRefreshHeader(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(context);
}

public int getLayoutId() {
return R.layout.layout_refresh_head;
}

protected void initView(Context context) {
mView = LayoutInflater.from(context).inflate(getLayoutId(), this);
toastTv = mView.findViewById(R.id.tv_toast);
rootLayout = mView.findViewById(R.id.rootLayout);
sdv_background = mView.findViewById(R.id.sdv_background);
PAGFile file = PAGFile.Load(context.getAssets(), DEFAULT_LOADING_FILE);
pagView = new PAGView(getContext());
LayoutParams params = new LayoutParams(UiUtils.dip2px(39), UiUtils.dip2px(39));
params.addRule(RelativeLayout.CENTER_IN_PARENT);
pagView.setLayoutParams(params);
pagView.setFile(file);
pagView.setRepeatCount(0);
rootLayout.addView(pagView);
}

public void setMarginTop(int marginTop) {
if (mView == null) return;
ViewGroup root = mView.findViewById(R.id.rootLayout);
if (root != null && root.getLayoutParams() instanceof MarginLayoutParams) {
MarginLayoutParams params = (MarginLayoutParams) root.getLayoutParams();
params.topMargin = marginTop;
root.setLayoutParams(params);
}
}

@NonNull
@Override
public View getView() {
return mView;
}

@Override
public SpinnerStyle getSpinnerStyle() {
return SpinnerStyle.Translate;
}

@Override
public void setPrimaryColors(@ColorInt int... colors) {

}

@Override
public void onInitialized(@NonNull RefreshKernel kernel, int height, int maxDragHeight) {

}

@Override
public void onMoving(boolean b, float v, int i, int i1, int i2) {
/**
* 【仅限框架内调用】手指拖动下拉(会连续多次调用,添加isDragging并取代之前的onPulling、onReleasing)
* @param isDragging true 手指正在拖动 false 回弹动画
* @param percent 下拉的百分比 值 = offset/footerHeight (0 - percent - (footerHeight+maxDragHeight) / footerHeight )
* @param offset 下拉的像素偏移量 0 - offset - (footerHeight+maxDragHeight)
* @param height 高度 HeaderHeight or FooterHeight (offset 可以超过 height 此时 percent 大于 1)
* @param maxDragHeight 最大拖动高度 offset 可以超过 height 参数 但是不会超过 maxDragHeight
*/
if (pagView != null && !pagView.isPlaying()) {
pagView.setProgress(v);
pagView.flush();
}
}

@Override
public void onReleased(@NonNull RefreshLayout refreshLayout, int i, int i1) {

}

@Override
public void onHorizontalDrag(float percentX, int offsetX, int offsetMax) {

}

@Override
public void onStartAnimator(RefreshLayout layout, int height, int extendHeight) {
LogUtils.e("RefreshHeader", "onStartAnimator");
if (pagView != null) pagView.play();
}

@Override
public int onFinish(RefreshLayout layout, boolean success) {
LogUtils.e("RefreshHeader", "onFinish");
if (pagView != null) pagView.stop();
return mFinishDuration;//延迟500毫秒之后再弹回
}

@Override
public boolean isSupportHorizontalDrag() {
return false;
}

@Override
public void onStateChanged(RefreshLayout refreshLayout, RefreshState oldState, RefreshState newState) {
switch (newState) {
case None:
case PullDownToRefresh:
case Refreshing:
if (pagView != null) pagView.setVisibility(VISIBLE);
if (toastTv != null) toastTv.setVisibility(GONE);
break;
case ReleaseToRefresh:
break;
case RefreshFinish:
if (pagView != null) pagView.setVisibility(GONE);
if (toastTv != null) toastTv.setVisibility(VISIBLE);
break;
}
}

public void setToastText(String str) {
if (StringUtils.isEmpty(str)) return;
if (toastTv != null) {
toastTv.setText(str);
}
}

public void setFinishDuration(int finishDuration) {
this.mFinishDuration = finishDuration;
}
}

Gson 数据解析

Gson 数据解析

0x01 Kotlin Gson 解析 data class 两条黄金法则:

1、 String 必须是可空类型 String?

2、 需要使用默认值,则全部字段都必须给予默认值,以满足kotlin对象有空的构造函数

0x02 手动解析Gson基础字段

1、msg 可空String解析 jsonReader.peek() == JsonToken.NULL

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import com.google.gson.Gson
import com.google.gson.JsonSyntaxException
import com.google.gson.TypeAdapter
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken
import okhttp3.ResponseBody
import retrofit2.Converter
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type

class ResponseBodyConverter(val gson: Gson, val type: Type) :
Converter<ResponseBody, BaseResponse<Any>> {
override fun convert(value: ResponseBody): BaseResponse<Any> {
val dataType = GenericsUtils.getParameterUpperBound(
0,
type as ParameterizedType
)
val baseResponse = BaseResponse<Any>()
val jsonReader = JsonReader(value.charStream())
try {
jsonReader.beginObject()
while (jsonReader.hasNext()) {
val name: String = jsonReader.nextName()
when (name) {
"code" -> {
baseResponse.code = jsonReader.nextString()
}
"msg" -> {
// this works, but do not do this.
if (jsonReader.peek() == JsonToken.NULL) {
jsonReader.nextNull()
baseResponse.msg = null
} else {
baseResponse.msg = jsonReader.nextString()
}
}
"data" -> {
val mapped: TypeAdapter<*>? = gson.getAdapter(TypeToken.get(dataType))
baseResponse.data = mapped?.read(jsonReader)
}
else -> {
jsonReader.skipValue()
}
}
}
} catch (e: IllegalStateException) {
throw JsonSyntaxException(e)
} catch (e: IllegalAccessException) {
throw AssertionError(e)
}
jsonReader.endObject()
return baseResponse
}
}

大量文本的浏览进度和浏览时长统计

大量文本的浏览进度和浏览时长统计

埋点需求,Android App 需要在onResume 和 onPause 方法中计算浏览的时长,同时上报浏览的进度。

浏览进度Rate具体的计算方式的具体过程:

1、在滚动屏幕过程中,通过 textContent?.viewTreeObserver?.addOnScrollChangedListener 来记录屏幕滚动的位置

2、在滚动监听里通过textContent?.getLocationOnScreen(location)获取在屏幕的具体位置,同时,

计算出visibleHeight = screenHeight - location[1] 当前文本的可见高度

3、当可见高度超过目标的高度,则认为已经全部浏览,rate = 100% ,同时移除滚动监听textContent?.viewTreeObserver?.removeOnScrollChangedListener(mScrollChangeListener)

核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
private var totalHeight: Int = 0
private var visibleHeight: Int = 0
private var screenHeight = 0

fun getViewRate(): String? =
if (totalHeight <= 0 || visibleHeight < 0) null
else "${(visibleHeight * 100 / totalHeight)}%"

private fun bindViewTreeObserver() {
cleanCache()
contentTv.post {
totalHeight = contentTv.height
screenHeight = ScreenUtils.getScreenHeight(context)
contentVisibleRate()
}
contentTv.viewTreeObserver.addOnScrollChangedListener(mScrollChangeListener)
}

private fun cleanCache() {
totalHeight = 0
screenHeight = 0
visibleHeight = 0
}

private val mScrollChangeListener = ViewTreeObserver.OnScrollChangedListener {
contentVisibleRate()
}

private fun contentVisibleRate() {
val location = IntArray(2)
contentTv.getLocationOnScreen(location)
visibleHeight = (screenHeight - location[1]).coerceAtLeast(visibleHeight)
if (visibleHeight >= totalHeight) {
visibleHeight = totalHeight
removeOnScrollChangedListener()
}
}

private fun removeOnScrollChangedListener() {
mScrollChangeListener?.let {
contentTv.viewTreeObserver.removeOnScrollChangedListener(mScrollChangeListener)
}
}

RecyclerViewHelper

RecyclerViewHelper

提供了注册加载更多,和判断是否不足一屏等工具方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager

object RecyclerViewHelper {
/**
* RecyclerView 注册加载更多监听
*/
@JvmStatic
fun addOnScrollListener(recyclerView: RecyclerView, loadMore: () -> Unit) {
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {
super.onScrollStateChanged(recyclerView, newState)
if (newState == RecyclerView.SCROLL_STATE_IDLE) {
val layoutManager = recyclerView.layoutManager
if (layoutManager is LinearLayoutManager) {
val lastPosition = layoutManager.findLastCompletelyVisibleItemPosition()
val count = layoutManager.itemCount
if (lastPosition >= count - 2) {
loadMore()
return@onScrollStateChanged
}
} else if (layoutManager is StaggeredGridLayoutManager) {
val spanCount = layoutManager.spanCount
val count = layoutManager.itemCount
val result = IntArray(spanCount)
layoutManager.findLastCompletelyVisibleItemPositions(result)
for (it in result) {
if (it >= count - spanCount - 1) {
loadMore()
return@onScrollStateChanged
}
}
} else if (layoutManager is GridLayoutManager) {
val spanCount = layoutManager.spanCount
val count = layoutManager.itemCount
val lastPosition = layoutManager.findLastCompletelyVisibleItemPosition()
if (lastPosition >= count - spanCount - 1) {
loadMore()
return@onScrollStateChanged
}

}
}
}
})
}


/**
* 判断是否一屏显示
*
* 错误或者空了返回 false
*/
@JvmStatic
fun isOneScreen(recyclerView: RecyclerView?): Boolean {
recyclerView?.let {
val layoutManager = recyclerView.layoutManager
if (layoutManager is LinearLayoutManager) {
// include GridLayoutManager
val count = layoutManager.itemCount
return count > 0 &&
layoutManager.findFirstCompletelyVisibleItemPosition() == 0 &&
layoutManager.findLastCompletelyVisibleItemPosition() == count - 1
} else if (layoutManager is StaggeredGridLayoutManager) {
val spanCount = layoutManager.spanCount
val count = layoutManager.itemCount
val last = IntArray(spanCount)
val first = IntArray(spanCount)
layoutManager.findLastCompletelyVisibleItemPositions(last)
layoutManager.findFirstCompletelyVisibleItemPositions(first)
return count > 0 && first.min() == 0 && last.max() == count - 1
}
}

return false
}

private fun IntArray.min(): Int {
if (this.isNotEmpty()) {
var result = this[0]
this.forEach {
if (result > it) result = it
}
return result
}
return -1
}

private fun IntArray.max(): Int {
if (this.isNotEmpty()) {
var result = this[0]
this.forEach {
if (result < it) result = it
}
return result
}
return -1
}
}

Problems专题:编译环境

Problems专题:编译环境

0x01 java.lang.AssertionError: Could not delete caches dir

CreateProcess error=206, El nombre del archivo o la extensión es demasiado largo

Caused by: java.lang.AssertionError: Could not delete caches dir YourProjectPath\build\kotlin\compileDebugTestingKotlin

临时解决

打开任务管理器,结束 java.exe 或者 OpenJDK Platform Binary

降级 Android Studio

Notice: This happens with the newer AndroidStudio 4.2.x.

Google hasn’t provide us a fix, so you’ll need to downgrade to an older version which works for you. 4.1.3 seems to be working fine.

参考链接:https://stackoverflow.com/questions/65832868/caused-by-java-lang-assertionerror-could-not-delete-caches-dir-yourproject-bui

0x02 Please close other application using ADB:Monitor, DDMS, Eclip

Warning:debug info can be unavailable. Please close other application using ADB:Monitor, DDMS, Eclipse.

方案一:

1
$ adb usb

方案二:

打开任务管理器,结束adb.exe进程。

方案三:

重启 adb 服务

1
2
$ adb kill-server
$ adb start-server

0x03 能安装apk却无法查看log[真机偶现]

能安装apk却无法在Logcat查看log,即使重启Android studio,重启adb服务都无法解决。最后通过重启手机搞定

0x04 platform-tools/api/api-versions.xml java.io.IOException: Stream closed

在 android studio 更新到 v2020.3.1 后遇到

1
cannot load api descriptions from ../Android/android-sdk/platform-tools/api/api-versions.xml java.io.IOException: Stream closed

问题的原因与类SdkUtils (请参阅the source file)相关。SdkUtils类具有对文件platform-tools/api/api-versions.xml的硬引用,但是在最新的平台工具(31.0.3)中,该文件不再存在。

从platforms/android-31/data/api-versions.xml复制文件到platform-tools/api/api-versions.xml。

如果是CI编译,可以尝试以下脚本:

1
2
3
4
5
steps:
- bash: |
echo Android sdk location: $ANDROID_SDK_ROOT
mkdir $ANDROID_SDK_ROOT/platform-tools/api/
cp $ANDROID_SDK_ROOT/platforms/android-30/data/api-versions.xml $ANDROID_SDK_ROOT/platform-tools/api/

0x05 Installed Build Tools revision 31.0.0 is corrupted. Remove and install again using the SDK Manager.

升级android sdk api 版本到31,适配android 12 ,遇到这个问题。当前开发环境:

android studio 版本: 2020.3.1

AGP 版本: 4.1.2 (classpath "com.android.tools.build:gradle:4.1.2"

SDK 版本

1
2
3
4
5
6
7
8
9
android {
compileSdkVersion 31
buildToolsVersion '31.0.0'

defaultConfig {
minSdkVersion 21
targetSdkVersion 31
}
}

SDK Manager更新对应版本都正常下载,编译过程出现异常

1
Installed Build Tools revision 31.0.0 is corrupted. Remove and install again using the SDK Manager.

是 Build Tools 升级之后,DX 变成了 D8。而 AGP 4.x 的版本使用的还是DX。

解决的方案:

1
2
3
4
5
# change below to your Android SDK path
cd ~/Library/Android/sdk/build-tools/31.0.0 \
&& mv d8 dx \
&& cd lib \
&& mv d8.jar dx.jar

C:\Users\user\AppData\Local\Android\Sdk\build-tools\31.0.0\d8.bat 改为 dx.bat
C:\Users\user\AppData\Local\Android\Sdk\build-tools\31.0.0\lib\d8.jar 改为 dx.jar

PS:也可以尝试升级 AGP 到 7.x

Your browser is out-of-date!

Update your browser to view this website correctly.&npsb;Update my browser now

×