此文主要是收集一些自己遇到的一些问题,方便自己总结,也方便他人解决问题!目前比较少,一点一点收集吧,路是一步一步走,坑也要一个一个的踩。
内存泄漏
Adapter刷新
SharedPreferences数据不能保存问题
GlideApp无法自动生成
自定义View生成失败
AndroidStudio升级到3.6异常
More than one file was found with OS independent path
MediaPlayer Error
自定义消息加密后服务端异常
横屏尺寸突然放大
EditText输入限制大写字母和数字
OnBackPressedDispatcher事件
ViewPager2#Adapter导致崩溃
bundletool解析aab后运行崩溃
1、内存泄漏
最近我负责维护的一个老项目遇到一个很奇怪的bug
,测试描述说某个Activity的第一次进入正常,第二次进入点击某个按钮的时候会崩溃。检查日志是View
空指针异常。我第一反应就是内存导致的,于是开始从这个方向检查。
首先通过Profiler
检查,发现没有异常,然后通过LeakCanary
继续检查,依然没有发现异常。最后通过方法调用的来分析。这个方法在第一次进入Activity
的时候可以执行无数次,第二次进来以后第一次也是正常的,但是当执行第二次的时候就崩溃了。后来在检查这个方法的时候,发现这个方法第二次执行的时候,是通过PopupWindow
点击回调来调用的,而PopupWindow
只是一个简单的私有成员变量,并不是静态的成员变量。后来在PopupWindow
实例化的时候发现PopupWindow
通过单例的方法实例化的,从而导致第二次接口回调的时候,使用的还是上一个接口回调,而上一个接口回调指向的是已经被回收的Activity,从而导致使用该PopupWindow
的Activity第二次进来使用它的时候,就崩溃了!
修改前:
private static RecommendPopup instance;
public static RecommendPopup getInstance(Activity context, IMapListener iMapListener){
if (instance == null)
instance = new RecommendPopup(context,iMapListener);
return instance;
}
修改后:
public RecommendPopup(Activity context,IMapListener iMapListener) {
super(context);
this.mActivity = context;
this.iMapListener = iMapListener;
}
解决思路: 这种问题,第一步确定初始化和调用的时候同一个对象指针
id
是否一致,如果不一致就去找异步,静态,单例。如果一致就去找异步或者其它逻辑谁动了它
2、Adapter刷新
前两天遇到一个奇怪的事情,Adapter
刷新没有反应。具体逻辑就是给Adapter
增加一个标志位,修改标志位以后刷新发现页面没有变化。添加断点检查确实进入了notifyDataSetChanged
方法,后来查看源码发现是因为自身数据没有变化导致adapter
没有进入convert
方法。查看源码:
/**
* Notify any registered observers that the data set has changed.
*
* <p>There are two different classes of data change events, item changes and structural
* changes. Item changes are when a single item has its data updated but no positional
* changes have occurred. Structural changes are when items are inserted, removed or moved
* within the data set.</p>
*
* <p>This event does not specify what about the data set has changed, forcing
* any observers to assume that all existing items and structure may no longer be valid.
* LayoutManagers will be forced to fully rebind and relayout all visible views.</p>
*
* <p><code>RecyclerView</code> will attempt to synthesize visible structural change events
* for adapters that report that they have {@link #hasStableIds() stable IDs} when
* this method is used. This can help for the purposes of animation and visual
* object persistence but individual item views will still need to be rebound
* and relaid out.</p>
*
* <p>If you are writing an adapter it will always be more efficient to use the more
* specific change events if you can. Rely on <code>notifyDataSetChanged()</code>
* as a last resort.</p>
*
* @see #notifyItemChanged(int)
* @see #notifyItemInserted(int)
* @see #notifyItemRemoved(int)
* @see #notifyItemRangeChanged(int, int)
* @see #notifyItemRangeInserted(int, int)
* @see #notifyItemRangeRemoved(int, int)
*/
public final void notifyDataSetChanged() {
mObservable.notifyChanged();
}
static class AdapterDataObservable extends Observable<AdapterDataObserver> {
...
public void notifyChanged() {
// since onChanged() is implemented by the app, it could do anything, including
// removing itself from {@link mObservers} - and that could cause problems if
// an iterator is used on the ArrayList {@link mObservers}.
// to avoid such problems, just march thru the list in the reverse order.
for (int i = mObservers.size() - 1; i >= 0; i--) {
mObservers.get(i).onChanged();
}
}
...
}
解决思路: 将标志位绑定到数据,而非绑定到Adapter
。
3、SharedPreferences数据不能保存问题
最近我负责维护的开源库UpdatePlugin有用户提了一个问题:第二次忽略版本的设置,成功以后在第二次进入应用的时候发现被忽略的版本号还停留在第一次的版本号。经过写demo
测试,确认这个是SharedPreferences
的锅。具体原因还在分析当中,有点烧脑。
先说一下解决方案吧,用户提供了一个解决方案:
修改前:
public static void saveIgnoreVersion(int versionCode) {
Set<String> ignoreVersions = getIgnoreVersions();
if (!ignoreVersions.contains(String.valueOf(versionCode))) {
ignoreVersions.add(String.valueOf(versionCode));
getUpdatePref().edit().putStringSet("ignoreVersions",ignoreVersions).apply();
}
}
// 用户提供修改方法:
public static void saveIgnoreVersion(int versionCode) {
Set<String> ignoreVersions = getIgnoreVersions();
if (!ignoreVersions.contains(String.valueOf(versionCode))) {
ignoreVersions.add(String.valueOf(versionCode));
getUpdatePref().edit().clear().putStringSet("ignoreVersions",ignoreVersions).apply();
}
}
// 最后修改方法:
/**
* 框架内部所提供使用的一些缓存数据存取:如下载进度、忽略版本。
* @author haoge
*/
public class UpdatePreference {
private static final String PREF_NAME = "update_preference";
public static List<String> getIgnoreVersions () {
String txt = getUpdatePref().getString("ignoreVersions", "");
if(TextUtils.isEmpty(txt))return new ArrayList<>();
txt = txt.replace("[","").replace("]","");
String[] result = txt.split(",");
// 杜绝 java.lang.UnsupportedOperationException
return new ArrayList<>(Arrays.asList(result));
}
public static void saveIgnoreVersion(int versionCode) {
List<String> ignoreVersions = getIgnoreVersions();
if (!ignoreVersions.contains(String.valueOf(versionCode))) {
ignoreVersions.add(String.valueOf(versionCode));
getUpdatePref().edit().putString("ignoreVersions",ignoreVersions.toString()).apply();
}
}
private static SharedPreferences getUpdatePref () {
return ActivityManager.get().getApplicationContext().getSharedPreferences(PREF_NAME,Context.MODE_PRIVATE);
}
}
选择修改存储方式是因为putStringSet
目前的bug
暂时还没有分析出具体原因,所以暂时先排除此方法,避免再次发生未知错误。
疑惑:
仔细看了源码关于保存的commitToMemory
和writeToFile
多遍,始终不明白为什么更新原有的Set<String>
以后,再次进入应用的时候数据会丢失?网上的解决方法基本上都是围绕commitToMemory
方法来分析,将mcr.changesMade
设置为默认的false
来避免后期被写入文件,但是如果通过修改mcr.changesMade
来避免数据写入文件的话,则mMap
无法保存需要存入的值,那么在存成功以后(还没有退出应用期间)是如何取到正确的值呢?
这个问题一直困扰着我,哪位大佬有思路请指点一下,谢谢?
文档提醒:
Note that you must not modify the set instance returned by this call. The consistency of the stored data is not guaranteed if you do, nor is your ability to modify the instance at all.
参考文章:
4、GlideApp 无法自动生成
最近复制同事其它项目的一个工具类到Kotlin
新项目,发现一个问题:GlideApp
无法自动生成,于是经过以下步骤实现:
- 每个
Model
下的gradle
均需配置apt
插件
apply plugin:'kotlin-kapt'
- 通过
Gradle
依赖Glide
,并且将注解依赖到每一个使用GlideApp
的Module
implementation 'com.github.bumptech.glide:glide:4.10.0'
// 使用GlideApp 的地方都需要添加
kapt 'com.github.bumptech.glide:compiler:4.10.0'
- 在每一个
Model
下的gradle
文件添加一下内容:
android{
compileOptions {
targetCompatibility 1.8
sourceCompatibility 1.8
}
kotlinOptions {
jvmTarget = '1.8'
}
}
- 然后先同步
sync now
,再重新编译Rebuild Project
即可
注意 由于gradle环境问题,不能保证每个人都能通过上述步骤解决依赖问题,只能是一个待校验的方案。
5、自定义View生成失败
是这样的,今天我本来打算自定义一个View,然后写好了之后加载直接崩溃了,我检查了一下代码:
class ProgressView (context: Context, attributeSet: AttributeSet? = null, defStyleAttr: Int = 0) : View(context, attributeSet, defStyleAttr) {
init{
...
}
...
}
异常日志:
Caused by: android.view.InflateException: Binary XML file line #15: Binary XML file line #15: Error inflating class com.junerver.videorecorder.ProgressView
Caused by: android.view.InflateException: Binary XML file line #15: Error inflating class com.junerver.videorecorder.ProgressView
Caused by: java.lang.NoSuchMethodException: <init> [class android.content.Context, interface android.util.AttributeSet]
我一看到init
关键字,以为是我的初始化有什么问题,检查了代码无果之后,打开了百度,得到这么一个结论:
看到这里我有点不信邪,于是把Kotlin
转java
代码一看,好吧,我服气了!
知错能改善莫大焉,意思就是晓得哪里错了改起来就容易了!根据之前的Kotlin实战填坑#构造方法的关键字经验,我将代码修改如下:
class ProgressView @JvmOverloads constructor(context: Context, attributeSet: AttributeSet?, defStyleAttr: Int) : View(context, attributeSet, defStyleAttr) {
init{
...
}
...
}
结果错误依然无动于衷,还是嚣张的爆红着!还好我还知道一个招式:
class ProgressView @JvmOverloads constructor(context: Context, attributeSet: AttributeSet? = null, defStyleAttr: Int = 0) : View(context, attributeSet, defStyleAttr) {
init{
...
}
...
}
之所以之前增加了JvmOverloads
注解还是失败,是因为没有对可空的字段设置默认值,在没有默认值的情况下,所以还是只有一种构造方法。这下终于解决了!
6、AndroidStudio升级到3.6异常
最近看见QQ群很多人都把AndroidStudio
升级到3.6
正式版看,我看见挺新颖的,于是趁着刚开工不是很忙的时候,就直接更新了,然后就遇到了以下问题:
C:\Users\Administrator\.gradle\caches\transforms-2\files-2.1\126332c53653ff7049fa8c511808692d\constraintlayout-2.0.0-beta1\res\values\values.xml:255:5-4197: AAPT: error: resource attr/flow_horizontalSeparator (aka com.cn.dcjt.firelibrary:attr/flow_horizontalSeparator) not found.
(1)尝试在gradle.properties
将aapt
注解关闭,失败
android.enableAapt2=false
(2)尝试检查项目中所有Module
的constraintlayout
版本号,发现版本号完全相同,不存在差异
(3)尝试检查项目中的flow_horizontalSeparator
属性,无发现。甚至将项目中所有的constraintlayout
布局修改为相对布局,依然无效。
(4)最后将constraintlayout
的版本号由beta版本修改为正式版本,解决!
// 修改前
"androidx.constraintlayout:constraintlayout:2.0.0-beta1"
// 修改后
"androidx.constraintlayout:constraintlayout:1.1.3"
7、发现有多个文件具有与操作系统无关的路径
错误日志:
More than one file was found with OS independent path 'META-INF/library_release.kotlin_module'
经过百度后看到
最近在引入两个
kotlin
写的aar
库时编译器报了这样的一个错误。说是打包时存在两个相同的文件,文件路径是META-INF/library_release.kotlin_module
。
后来经过检查,发现是com.github.lygttpod:SuperTextView:2.4.2
与com.github.chenBingX:SuperTextView:v3.2.5.99
两套框架冲突了!知道原因后,解决就简单了:
- 两个框架二选一
- 在项目的主
Module
即打包的壳组件(一般是app
)增加如写代码:
android {
packagingOptions {
exclude 'META-INF/*.kotlin_module'
}
}
参考:
8、MediaPlayerError
错误日志:
MediaPlayerNative: error (1, -2147483648)
MediaPlayer: Error (1,-2147483648)
使用安卓10手机播放音频,音频地址的host为http,且无域名,在华为手机播放正常,在小米、一加等品牌测试有上述异常。通过百度增加了忽略网络完全检测依然失败。
<application
android:name=".base.App"
android:allowBackup="true"
android:icon="@mipmap/app_logo"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_config"
android:usesCleartextTraffic="true"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/app_logo"
android:supportsRtl="true"
android:theme="@style/AppTheme">
后来查看日志,为位置的系统异常,因此直接使用ExoPlayer。替换了MediaPlayer。
9、自定义消息加密后服务端异常
前段时间遇到一个问题,自定义的json
格式字符串加密后,发送到服务端(我们公司后端为node js
开发)之后乱码。经过检查,加密后的密文是通过POST
请求发送的,密文放在RequestBody
内,且未设置编码格式。后来经过和服务端开发人员调试,发现服务端对密文使用了escape
函数进行了编码。而我一开始使用了网上的各种方式来兼容这种编码,测试了很多次之后发现都不对,自己重新写了兼容该方法的解码/编码方法之后,才发现原来escape
函数已经被文档标注为非推荐编码方法,而推荐的编码方法为decodeURI
函数。推荐的decodeURI
函数进行编码,我们使用URLDecoder#decode
函数解码也不会有问题。如果大家以后遇到类似的问题可以要求后台使用decodeURI
函数进行编码了。
具体解决过程见编码/解码那些事。
10、横屏尺寸突然放大
最近遇到一个问题,某个页面横屏时,设置的尺寸突然放大了一倍。
这种情况第一反应就是AutoSize
造成的,但是当我注释了屏幕适配参数后依然如此:
后来以为是ViewBinding
关联了两个layout
文件的bug
,但是注释ViewBinding
,直接通过setContentView
函数来加载页面依然不行。
最后发现还是AutoSize
造成的,原因是注释了框架的gradle
依赖才发现。没有第一时间发现,是因为框架拥有默认适配尺寸(360*640)。而横屏的时候使用全局根据竖屏宽高比计算出的density
就出现了上面描述的异常。找到问题后,我们可以通过查看文档,使Activity
实现空接口CancelAdapt
,直接放弃了该页面的适配,至此解决了该问题。
11、EditText输入限制大写字母和数字
一开始看着这个需求心里想的是自定义键盘,后来发现可以使用系统软键盘实现这个功能,需要如下三步:
1 EditText
定义digits
属性实现,代码如下:
<EditText
android:id="@+id/tv_code"
style="@style/edt_text"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginLeft="4dp"
android:layout_weight="1"
android:hint="请输入"
android:maxLength="17"
android:digits="0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"/>
2、调用EditText
的setTransformationMethod
函数
tvCode.transformationMethod = object : ReplacementTransformationMethod() {
override fun getOriginal() = charArrayOf('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z')
override fun getReplacement() = charArrayOf('A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z')
}
现在页面显示的就是大写字母加数字了,但是tvCode#text
依然包含小写字母,我们取值的时候需要加一个过滤,代码如下:
tvCode.text.toString().toUpperCase()
如此操作即可实现EditText输入大写字母和数字了。
11、OnBackPressedDispatcher
事件
之前有一个需求,需要Fragment监听Activity的返回事件,通过面向百度编程,得到了一个答案:【背上Jetpack之OnBackPressedDispatcher】Fragment 返回栈预备篇
requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner,object :
OnBackPressedCallback(true){
override fun handleOnBackPressed() {
// TODO Something
}
})
后来通过测试,so easy,上面几行代码就把问题解决了。
但是测试后来告诉我Fragment所在的Activity无法返回,根据断点检测,返回Activity#onKeyUp
函数始终返回true
,让我百思不得姐解。明明是事件分发,怎么变成了返回事件拦截呢?
然后查看源码,发现OnBackPressedDispatcher#onBackPressed
函数才明白逻辑:
@MainThread
public void onBackPressed() {
Iterator<OnBackPressedCallback> iterator =
mOnBackPressedCallbacks.descendingIterator();
while (iterator.hasNext()) {
OnBackPressedCallback callback = iterator.next();
if (callback.isEnabled()) {
callback.handleOnBackPressed();
return;
}
}
if (mFallbackOnBackPressed != null) {
mFallbackOnBackPressed.run();
}
}
上面的代码可以看到,如果当前有OnBackPressedCallback#isEnabled
函数返回true
,则消费掉当前的返回事件,此处会有拦截返回时事件的异常;其次,就算我们使用的Fragment
在添加返回事件监听的时候,将OnBackPressedCallback
设置为true
,FragmentManager#updateOnBackPressedCallbackEnabled
依然会修改这个值,导致我们不一定能准确收到返回事件的消息。比如ViewPager2
容器就有这个异常。
private void updateOnBackPressedCallbackEnabled() {
// Always enable the callback if we have pending actions
// as we don't know if they'll change the back stack entry count.
// See handleOnBackPressed() for more explanation
synchronized (mPendingActions) {
if (!mPendingActions.isEmpty()) {
mOnBackPressedCallback.setEnabled(true);
return;
}
}
// This FragmentManager needs to have a back stack for this to be enabled
// And the parent fragment, if it exists, needs to be the primary navigation
// fragment.
mOnBackPressedCallback.setEnabled(getBackStackEntryCount() > 0
&& isPrimaryNavigation(mParent));
}
所以Fragment
如果想要监听返回事件,并且打算在Fragment
使用OnBackPressedCallback
抽象类来实现的话,应用情景就是容器只存在一个Fragment
,并且这个Fragment
没有使用Navigation
等导航。
综上所述,项目中不建议使用OnBackPressedCallback
来实现返回事件监听。
12、安卓10Scoped Storage
填坑
最近在项目中有一个需求,从相册中选择一张照片展示并上传。实现很简单,几句代码就OK。
// ① 声明一个ActivityResultLauncher变量
private lateinit var mLauncher: ActivityResultLauncher<Intent>
// ② 初始化ActivityResultLauncher
override fun onAttach(context: Context) {
super.onAttach(context)
mLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
// 处理图片
}
}
// ③ 通过ActivityResultLauncher选择图片
val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI).apply {
setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/*")
}
mLauncher.launch(intent)
拿到图片以后我正常显示,OK,开始下一步——上传服务器。不过由于安卓10开始存储策略修改为分区存储了,所以我们还不能直接把uri转为path,否则在获取文件的时候会报权限拒绝的异常。
java.io.FileNotFoundException: ** open failed: EACCES (Permission denied) 此时我们可以为应用添加
android:requestLegacyExternalStorage="true"
标志,如下:
<application
android:name=".AppApplication"
android:allowBackup="false"
android:icon="@mipmap/ic_logo"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:supportsRtl="true"
android:requestLegacyExternalStorage="true"
android:theme="@style/base_AppTheme"
tools:ignore="UnusedAttribute">
此时已经可以解决了,但是为了谨慎起见,我们还是加上FileProvider
为好。
<application
...
>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepath" />
</provider>
<application/>
然后在res下新建xml
目录,然后新建ilepath
文件
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path name="my_images" path="images/"/>
</paths >
最后对得到的uri赋予读写权限,将持有Uri
的Intent
进行设置:
it.data?.let { intent ->
intent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
intent.data?.let { uri ->
// uri转文件并上传图片
}
}
这样就可以了吗?requestLegacyExternalStorage
始终是临时手段,现在谷歌应用市场已经要求上架应用目标版本必须是30了,所以还是适配比较好。
那么如何适配呢?经测试,可以直接将文件通过流的方式复制到应用私有目录下,然后就可以自由获取文件了,实现方式如下:
/**
* 将图片复制到项目私有目录
*/
fun uriToFIle(context: Context,uri: Uri):File?{
val imgFile:File = context.getExternalFilesDir("image")?:context.filesDir
if (!imgFile.exists()){
imgFile.mkdir()
}
try {
val file = File(imgFile.absolutePath + File.separator +
System.currentTimeMillis() + ".jpg")
// 使用openInputStream(uri)方法获取字节输入流
val fileInputStream = context.contentResolver.openInputStream(uri)!!
val fileOutputStream = FileOutputStream(file)
val buffer = ByteArray(1024)
var byteRead = 0
while (-1 != fileInputStream.read(buffer).also { byteRead = it }) {
fileOutputStream.write(buffer, 0, byteRead)
}
fileInputStream.close()
fileOutputStream.flush()
fileOutputStream.close()
Log.e("uriToFIle","work done")
return file
// 文件可用新路径 file.getAbsolutePath()
} catch (e:Exception ) {
e.printStackTrace()
return null
}
}
// 调用方式
val file = Luban.with(context).load(FileUtil.uriToFIle(context, uri)!!).ignoreBy(256)
.get().first()
注意:① Luban
压缩uri图片有问题,应避免直接将uri提供给Luban
。
② uriToFIle
函数写得比较丑,抛砖引玉了!
13、ViewPager2#Adapter
导致崩溃
背景情况是这样的,首页底部显示三个tab
——主页、账单、我的,现在需求是未登录用户进入主页时显示未登录的UI,且点击账单和我的两个tab的话进入登录页。于是有了如下代码:
viewpager.adapter = object : FragmentStateAdapter(this@MainActivity){
override fun getItemCount() = 3
override fun createFragment(position: Int):Fragment{
return if(!UserInfoManager.getInstance().isLogin){
list[3]
}else{
list[position]
}
}
}
结果收获了如下异常:
java.lang.IllegalStateException: Fragment already added
at androidx.fragment.app.Fragment.setInitialSavedState(Fragment.java:709)
at androidx.viewpager2.adapter.FragmentStateAdapter.ensureFragment(FragmentStateAdapter.java:269)
at androidx.viewpager2.adapter.FragmentStateAdapter.onBindViewHolder(FragmentStateAdapter.java:175)
at androidx.viewpager2.adapter.FragmentStateAdapter.onBindViewHolder(FragmentStateAdapter.java:67)
at androidx.recyclerview.widget.RecyclerView$Adapter.onBindViewHolder(RecyclerView.java:7065)
at androidx.recyclerview.widget.RecyclerView$Adapter.bindViewHolder(RecyclerView.java:7107)
at androidx.recyclerview.widget.RecyclerView$Recycler.tryBindViewHolderByDeadline(RecyclerView.java:6012)
at androidx.recyclerview.widget.RecyclerView$Recycler.tryGetViewHolderForPositionByDeadline(RecyclerView.java:6279)
at androidx.recyclerview.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:6118)
at androidx.recyclerview.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:6114)
at androidx.recyclerview.widget.LinearLayoutManager$LayoutState.next(LinearLayoutManager.java:2303)
at androidx.recyclerview.widget.LinearLayoutManager.layoutChunk(LinearLayoutManager.java:1627)
at androidx.recyclerview.widget.LinearLayoutManager.fill(LinearLayoutManager.java:1587)
at androidx.recyclerview.widget.LinearLayoutManager.onLayoutChildren(LinearLayoutManager.java:665)
at androidx.recyclerview.widget.RecyclerView.dispatchLayoutStep2(RecyclerView.java:4134)
at androidx.recyclerview.widget.RecyclerView.dispatchLayout(RecyclerView.java:3851)
at androidx.recyclerview.widget.RecyclerView.onLayout(RecyclerView.java:4404)
at android.view.View.layout(View.java:23750)
at android.view.ViewGroup.layout(ViewGroup.java:7277)
at androidx.viewpager2.widget.ViewPager2.onLayout(ViewPager2.java:527)
at android.view.View.layout(View.java:23750)
at android.view.ViewGroup.layout(ViewGroup.java:7277)
at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1829)
at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1673)
at android.widget.LinearLayout.onLayout(LinearLayout.java:1582)
at android.view.View.layout(View.java:23750)
at android.view.ViewGroup.layout(ViewGroup.java:7277)
at android.widget.FrameLayout.layoutChildren(FrameLayout.java:332)
at android.widget.FrameLayout.onLayout(FrameLayout.java:270)
at android.view.View.layout(View.java:23750)
at android.view.ViewGroup.layout(ViewGroup.java:7277)
at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1829)
at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1673)
at android.widget.LinearLayout.onLayout(LinearLayout.java:1582)
at android.view.View.layout(View.java:23750)
at android.view.ViewGroup.layout(ViewGroup.java:7277)
at android.widget.FrameLayout.layoutChildren(FrameLayout.java:332)
at android.widget.FrameLayout.onLayout(FrameLayout.java:270)
at android.view.View.layout(View.java:23750)
at android.view.ViewGroup.layout(ViewGroup.java:7277)
at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1829)
at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1673)
at android.widget.LinearLayout.onLayout(LinearLayout.java:1582)
at android.view.View.layout(View.java:23750)
at android.view.ViewGroup.layout(ViewGroup.java:7277)
at android.widget.FrameLayout.layoutChildren(FrameLayout.java:332)
at android.widget.FrameLayout.onLayout(FrameLayout.java:270)
at com.android.internal.policy.DecorView.onLayout(DecorView.java:1099)
at android.view.View.layout(View.java:23750)
at android.view.ViewGroup.layout(ViewGroup.java:7277)
at android.view.ViewRootImpl.performLayout(ViewRootImpl.java:3709)
at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:3161)
at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:2222)
at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:9123)
网上有人说重写getItemId
函数即可,修改如下:
...
override fun getItemId(position: Int): Long {
return if(position!=0){
position*100L
}else{
if(!UserInfoManager.getInstance().isLogin){
3*1000L
}else{
50*1000L
}
}
}
...
结果错误没有任何变化,将ItemCount
返回4,createFragment函数内position与Fragment一对一的绑定解决问题:
viewpager.adapter = object : FragmentStateAdapter(this@MainActivity){
override fun getItemCount() = 4
override fun createFragment(position: Int) = list[position]
}
14、bundletool
解析aab
后运行崩溃
众所周知,谷歌从2021年9月1日起,要求新上架的安装包必须是Android App Bundle
。所以我们会在打包以后进行测试,没有问题再进行上架。然后去年的时候,遇到了一个棘手的问题,就是项目打包apk
以后安装能正常使用,但是通过bundletool解析成apk
后,运行时会发生MMKV初始化崩溃,日志如下:
Caused by: java.lang.UnsatisfiedLinkError: dalvik.system.PathClassLoader[DexPathList[[zip file "/system/framework/android.test.mock.jar", zip file "/system/framework/android.test.runner.jar", zip file "/data/app/com.chehejia.oc.m01-TBWDg7lrljgGQwQnlsmgcQ==/base.apk"],nativeLibraryDirectories=[/data/app/com.chehejia.oc.m01-TBWDg7lrljgGQwQnlsmgcQ==/lib/arm64, /data/app/com.chehejia.oc.m01-TBWDg7lrljgGQwQnlsmgcQ==/base.apk!/lib/arm64-v8a, /system/lib64, /vendor/lib64, /product/lib64]]] couldn't find "libmmkv.so"
后来经测试发现,通过GooglePlay
上传的aab,下载指定型号的手机和通用型号的手机对应的apk
文件来安装都没有问题。所以当时以为是开源的bundletool
存在bug
,而GooglePlay
使用的解析工具是优化后的版本,所以一直没有在意。后来看到有人推荐使用ReLinker可以解决上述问题,代码如下:
MMKV.initialize(filesDir.absolutePath + "/mmkv") { libName -> ReLinker.loadLibrary(this@App, libName) }
如此解决了上述的问题。那么问题又来了,这个库做了什么解决的这个问题呢?
This confirmed that Google Play was not the issue, the issue was with Android’s PackageManager installation process. At some point during the installation, something would go wrong and the native libraries inside of the APK would not be extracted.
这证实了 Google Play 不是问题,问题在于 Android 的 PackageManager 安装过程。 在安装过程中的某个时刻,会出现问题,无法提取 APK 内的本机库。
We started to log the installer package name in our crashes and quickly figured out that, yes, users were installing the app from various sources, and each new UnsatisfiedLinkError was coming from a manually installed app where the user mistakenly installed the wrong flavor for their device’s architecture. This was the final “gotcha”, and we were relieved that it had a very simple explanation.
我们开始在崩溃中记录安装程序包名称,并很快发现,是的,用户正在从各种来源安装应用程序,并且每个新的 UnsatisfiedLinkError 都来自手动安装的应用程序,用户错误地为他们的设备安装了错误的体系结构。 这是最后的“陷阱”,我们松了一口气,因为它有一个非常简单的解释。
原来是我们经过非系统的安装方式安装时,PackageManager
错误的选择了abi
架构(例如armeabi
、arm64-v8a
等架构),于是代码从安装和架构查找两个方面着手,解决了这个问题。
后记
1----本文由苏灿烤鱼整理,将不定期更新。
2----如果有什么想要交流的,欢迎留言。也可以加微信:Vicent_0310
3----个人能力有限,如有不正之处欢迎大家批评指证,必定虚心改正