我遇到的那些bug(持续更新中)

2,667 阅读15分钟

此文主要是收集一些自己遇到的一些问题,方便自己总结,也方便他人解决问题!目前比较少,一点一点收集吧,路是一步一步走,坑也要一个一个的踩。

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暂时还没有分析出具体原因,所以暂时先排除此方法,避免再次发生未知错误。

疑惑: 仔细看了源码关于保存的commitToMemorywriteToFile多遍,始终不明白为什么更新原有的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.

参考文章:

putStringSet数据不能保存问题

4、GlideApp 无法自动生成

最近复制同事其它项目的一个工具类到Kotlin新项目,发现一个问题:GlideApp无法自动生成,于是经过以下步骤实现:

  1. 每个Model下的gradle均需配置apt插件
apply plugin:'kotlin-kapt'
  1. 通过Gradle依赖Glide,并且将注解依赖到每一个使用GlideAppModule
implementation 'com.github.bumptech.glide:glide:4.10.0'
// 使用GlideApp 的地方都需要添加
kapt 'com.github.bumptech.glide:compiler:4.10.0'
  1. 在每一个Model下的gradle文件添加一下内容:
 android{
      compileOptions {
        targetCompatibility 1.8
        sourceCompatibility 1.8
    }

    kotlinOptions {
        jvmTarget = '1.8'
    }
 } 
  1. 然后先同步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关键字,以为是我的初始化有什么问题,检查了代码无果之后,打开了百度,得到这么一个结论:

网络结果

看到这里我有点不信邪,于是把Kotlinjava代码一看,好吧,我服气了!

知错能改善莫大焉,意思就是晓得哪里错了改起来就容易了!根据之前的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.propertiesaapt注解关闭,失败

android.enableAapt2=false

(2)尝试检查项目中所有Moduleconstraintlayout版本号,发现版本号完全相同,不存在差异

(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.2com.github.chenBingX:SuperTextView:v3.2.5.99两套框架冲突了!知道原因后,解决就简单了:

  • 两个框架二选一
  • 在项目的主Module即打包的壳组件(一般是app)增加如写代码:
android {
    packagingOptions {
       exclude 'META-INF/*.kotlin_module'
   }
}

参考:

blog.csdn.net/yinxing2008…

deskid.github.io/2019/03/05/…

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造成的,但是当我注释了屏幕适配参数后依然如此:

注释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、调用EditTextsetTransformationMethod函数

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,上面几行代码就把问题解决了。

image.png

但是测试后来告诉我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设置为trueFragmentManager#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赋予读写权限,将持有UriIntent进行设置:

 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 都来自手动安装的应用程序,用户错误地为他们的设备安装了错误的体系结构。 这是最后的“陷阱”,我们松了一口气,因为它有一个非常简单的解释。

The Perils of Loading Native Libraries on Android

原来是我们经过非系统的安装方式安装时,PackageManager错误的选择了abi架构(例如armeabiarm64-v8a等架构),于是代码从安装和架构查找两个方面着手,解决了这个问题。


后记

1----本文由苏灿烤鱼整理,将不定期更新。

2----如果有什么想要交流的,欢迎留言。也可以加微信:Vicent_0310

3----个人能力有限,如有不正之处欢迎大家批评指证,必定虚心改正