Skip to content

Latest commit

 

History

History
687 lines (458 loc) · 21 KB

问题纪要.md

File metadata and controls

687 lines (458 loc) · 21 KB

图片上传

java.net.ProtocolException: expected 4830 bytes but received 8192

在上传图片的时候,偶发性的报错,而且,这个问题只存在部分手机中。

原因

图片上传的时候,给的size 大小 和实际上传的大小不一致,导致服务器关闭了流的传输。

造成这种情况,很多,可能是多个线程对文件进行读写,文件还没有写完,就开始上传了。

解决方式

能找到问题的原因更好,如果早不到问题的原因。最好的方式copy一份要上传的问题。上传copy的文件。

content-length = -1 导致下载的逻辑问题

koltin if else if 导致的问题

koltin的if else 是一个表达式,但是下面的代码会出现问题:

  if (true){
        "1"
    }else if (false){
        "2"
    }else{
        "3"
    }.let {
        println(it)
    }

你会发现,没有任何输出

如果是下面这种写法

  if (true){
        "1"
    } else{
        "3"
    }.let {
        println(it)
    }

那么又是正常的

为什么会这样?猜测是 koltin 的bug ,这个bug 匪夷所思

kolint 的 interface clone 接口问题

腾讯 x5 浏览,不支持浏览在线文档引发的问题。

当网页中,的某个链接地址是 pdf或者其它类型的文件时,由于腾讯x5 引擎处理不了于是 DownloadListener 会被回调,告诉你引擎处理不了,但是 问题代码就出现:

   webView?.setDownloadListener { url: String?, userAgent: String?, contentDisposition: String?, mimetype: String?, contentLength: Long ->
            val intent = Intent()
            intent.action = "android.intent.action.VIEW"
            val contentUrl = Uri.parse(url)
            intent.data = contentUrl
            startActivity(intent)
        }

这样,造成了一个死循环了。

解决方式是,对于不能处理的文件直接走文件预览的流程。

部分 OPPO 和一加手机无法启动摄像头

代码如下:

 public void startOpenCameraVideo() {
        Intent cameraIntent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE);
        if (cameraIntent.resolveActivity(getPackageManager()) != null) {
            File cameraFile = PictureFileUtils.createCameraFile(this, config.mimeType ==
                            PictureConfig.TYPE_ALL ? PictureConfig.TYPE_VIDEO : config.mimeType,
                    outputCameraPath, config.suffixType);
            cameraPath = cameraFile.getAbsolutePath();
            Uri imageUri = parUri(cameraFile);
            cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
            cameraIntent.putExtra(MediaStore.EXTRA_DURATION_LIMIT, config.recordVideoSecond);
            cameraIntent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, config.videoQuality);
            startActivityForResult(cameraIntent, PictureConfig.REQUEST_CAMERA);
        }
    }

上面的代码也是Android官方的写法,在大部分的手机上都没啥问题,但是偏偏部分的 OPPO 手机无法唤起

解决方法

在manifest.xml中添加如下的配置

   <queries>
        <intent>
            <action android:name="android.media.action.IMAGE_CAPTURE" />
        </intent>
    </queries>

https://developer.android.com/about/versions/11/privacy/package-visibility#all-apps

但是问题在于,摄像头一直作为系统应用的,应该满足其协议应用,但是,OPPO的新系统,确把拍照,摄像机做为一个应用级别的app了,需要配置软件包的可见性。真尼玛的坑1

字符串中,关于肉眼不可见,但是有长度的字符

\u200b

这个字符的长度为 0

问题场景

在做二维码扫码的时候,扫到一个这样的二维码:

image

然后浏览无法跳转,扫除的二维码字符串如下: ​http://qm.qq.com/cgi-bin/qm/qr?k=ubrTTgpncqjEri8kxX67qfyBACSIemjf

怎么看都没什么问题。但最终发现,http前面还有一个'\u200b'的字符,但是肉眼不可见。

如何让Message 快速的被执行

Handler 同步屏障机制

一种简单的消息有限执行的机制。

https://blog.csdn.net/asdgbc/article/details/79148180

EventBus 的代码阅读

核心的代码就是 EventBus 里面的代码;

public void register(Object subscriber)

在注册的时候,通过反射,找到接收event 的方法,然后封装成一个 Subscription。

post 的时候,就是找 Subscription 。

原理很简单。

关于获取状态栏高度的

测试提出一个 视觉bug ,在三星 s20+ 手机上,内容延伸到 状态栏下面,造成内容的遮挡。

于是我看到了这样的代码:

        private fun getLocalStatusBarHeight(context: Context): Int {
            var height = 0   
            val identifier = context.resources.getIdentifier("status_bar_height", "dimen", "android")
            var xdpi = context.resources.getDisplayMetrics().xdpi
            if (identifier > 0) {
                height = context.resources.getDimensionPixelSize(identifier)
            }
            if (height == 0) {
                height = context.resources.getDimensionPixelSize(R.dimen.common_54)
            }
            return height
        }

关键是这样一行

val identifier = context.resources.getIdentifier("status_bar_height", "dimen", "android")

显然,再部分手机上,获取的这个值是不正确的。

那么正确的方式是什么呢?

  public static void setOnApplyWindowInsetsListener(@NonNull final View v,
            final @Nullable OnApplyWindowInsetsListener listener) {
        if (Build.VERSION.SDK_INT >= 21) {
            Api21Impl.setOnApplyWindowInsetsListener(v, listener);
        }
    }

当然,当view 已经 成功绑定 window 后,可以这样的获取

    /**
     * Provide original {@link WindowInsetsCompat} that are dispatched to the view hierarchy.
     * The insets are only available if the view is attached.
     * <p>
     * On devices running API 20 and below, this method always returns null.
     *
     * @return WindowInsetsCompat from the top of the view hierarchy or null if View is detached
     */
    @Nullable
    public static WindowInsetsCompat getRootWindowInsets(@NonNull View view) {
        if (Build.VERSION.SDK_INT >= 23) {
            return Api23Impl.getRootWindowInsets(view);
        } else if (Build.VERSION.SDK_INT >= 21) {
            return Api21Impl.getRootWindowInsets(view);
        } else {
            return null;
        }
    }

Android 官方已经给出了解决方案,就是不查文档

延伸知识,关于 WindowInsets

inset 简单的理解,再全面屏幕时代,屏幕上有系统的状态栏,底部的导航栏。这些地方的尺寸需要有个东西去描述,inset 就是描述这个的尺寸。

参考资源:

https://mp.weixin.qq.com/s/DEI4bcmKkRBySUjO2AYEJA

https://developer.android.com/guide/topics/display-cutout?hl=zh-cn

https://medium.com/androiddevelopers/windowinsets-listeners-to-layouts-8f9ccc8fa4d1

https://juejin.cn/post/6844904006343458830

WebView 奇异崩溃

Using WebView from more than one process at once with the same data directory is not supported. https://crbug.com/558377

org.chromium.android_webview.AwBrowserProcess.b(PG:11)

这样的崩溃日志,根据Android 官方的说明

public static void setDataDirectorySuffix (String suffix)

Define the directory used to store WebView data for the current process. The provided suffix will be used when constructing data and cache directory paths. If this API is not called, no suffix will be used. Each directory can be used by only one process in the application. If more than one process in an app wishes to use WebView, only one process can use the default directory, and other processes must call this API to define a unique suffix.

This means that different processes in the same application cannot directly share WebView-related data, since the data directories must be distinct. Applications that use this API may have to explicitly pass data between processes. For example, login cookies may have to be copied from one process's cookie jar to the other using CookieManager if both processes' WebViews are intended to be logged in.

Most applications should simply ensure that all components of the app that rely on WebView are in the same process, to avoid needing multiple data directories. The disableWebView() method can be used to ensure that the other processes do not use WebView by accident in this case.

This API must be called before any instances of WebView are created in this process and before any other methods in the android.webkit package are called by this process.

那么处理也简单,在Application 中


 protected void attachBaseContext(Context base) {
       
            String processName = getProcessName();
            if (!TextUtils.equals(context.getPackageName(), processName)) {
                String  suffix = TextUtils.isEmpty(processName) ? getPackageName() : processName;
                WebView.setDataDirectorySuffix(suffix);
              
            }
    }



但是虽然这样处理,仍然有零星的一些崩溃,百思不得其解

考虑到:当app崩溃的时候,一般会重启,这个时候,一个进程正在关闭,另一个进程正在重启,于是。。。

https://www.yisu.com/zixun/445583.html

数据对比

看看下面的代码有没有问题

 Arrays.sort(points, new Comparator<int>() {
            @Override
            public int compare(int o1, int o2) {
                return o1 - o2;
            }
        });

咋一看 好像没啥问题,但是碰到 这样的数据 ,-2147483645 和 2147483647 对比的时候,就出错了,原因也很简单,越界了,也就是说,减法计算有越界的风险,正确的做法如下:

 Arrays.sort(points, new Comparator<int>() {
            @Override
            public int compare(int o1, int o2) {
                if(o1 > 0 && o2 <0){
                    return 1;
                }else if(o1 <0 && o2 >0){
                    return -1;
                }else{
                  return o1 - o2;
                }
            }
        });

Activity 重建时引发的问题

当Activity 因为各种原因被系统回收后重启后,注意一些问题,特别是有 Fragment 的情况

看下面的场景

一个Fragment

class SplashFragment : Fragment() {

    companion object {
        const val TAG= "splash"
        @JvmStatic
        fun newInstance(): SplashFragment {
            return SplashFragment()
        }
    }

    interface OnSplashFinishListener {
        fun pageFinish()
    }

    private var mOnSplashFinishListener: OnSplashFinishListener? = null
    override fun onAttach(context: Context) {
        super.onAttach(context)
        if (context is OnSplashFinishListener) {
            mOnSplashFinishListener = context
        }
    }

    override fun onDetach() {
        super.onDetach()
        mOnSplashFinishListener = null
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        initData()
    }

    private fun initData() {
        // 复杂的业务处理
        .....
         mOnSplashFinishListener?.pageFinish()
    }
}

对于 Activity

public class HomeActivity extends BaseActivity
        implements  SplashFragment.OnSplashFinishListener{
private View view;
 protected void onCreate(@Nullable Bundle savedInstanceState) {
    ....
    view = findViewById(R.id.view)
     Fragment f = getSupportFragmentManager().findFragmentByTag(SplashFragment.TAG);
        if (f == null) {
            getSupportFragmentManager().beginTransaction()
                    .add(SplashFragment.newInstance(), SplashFragment.TAG)
                    .commitNowAllowingStateLoss();
        }
 }

 public void pageFinish() {
      view.setBackgroundColor(getResources().getColor(R.color.common_white));
 }

}

上面的代码正常的执行是没有任何问题的,但是当 Activity 重建时,从下面的代码可以看出,Fragment 是先创建 的,那么呵呵呵,我们在子类Activity中重写 onCreate() 然后setContentView 是在后面执行的。

    /**
     * {@inheritDoc}
     *
     * Perform initialization of all fragments.
     */
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mFragmentLifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_CREATE);
        mFragments.dispatchCreate();
    }

的事物可能先执行Fragment的执行过程和Activity 几乎是同时进行的,mOnSplashFinishListener?.pageFinish() 会很快的执行后,因为 view 还没有初始化,导致空指针异常,处理的方式也很简单,在

super.onCreate(savedInstanceState) 中,先移除相关的Fragment


public class HomeActivity extends BaseActivity
        implements  SplashFragment.OnSplashFinishListener{
private View view;
 protected void onCreate(@Nullable Bundle savedInstanceState) {

      // 先移除,在初始化后,在添加
   Fragment f = getSupportFragmentManager().findFragmentByTag(SplashFragment.TAG);
        if (f != null) {
            getSupportFragmentManager().beginTransaction()
                    .remove(f)
                    .commitNowAllowingStateLoss();
        }


    ....
    super.obCreate(savedInstanceState)
 
      view = findViewById(R.id.view)
     Fragment f = getSupportFragmentManager().findFragmentByTag(SplashFragment.TAG);
        if (f == null) {
            getSupportFragmentManager().beginTransaction()
                    .add(SplashFragment.newInstance(), SplashFragment.TAG)
                    .commitNowAllowingStateLoss();
        }
 }

 public void pageFinish() {
      view.setBackgroundColor(getResources().getColor(R.color.common_white));
 }

}

     

webview 输入法遮挡问题

在使用腾讯 tbs 的时候,如果tbs 内核加载成功,那么久不会出现这个问题,但是当tbs 内核加载失败的时候,必现。

当使用原生的 webview 的时候,这个bug 是谷歌的 https://issuetracker.google.com/issues/36911528

到目前为止也没有修复,下面是绕过bug 的方式, https://cloud.tencent.com/developer/article/1179343

这种方式的原理是,当软件盘弹出时,重新设置布局的高度,逼迫webview 重绘。

请注意 这种方式不适用 腾讯的tbs,如果你使用腾讯的tbs,应该按照他们相关的规则进行配置。

Uri 的使用

关于 Uri 是什么,不过多的介绍,我想说的是,构造和解析 Uri 应该使用标准库

我在review代码中,见到了下面恐怖的代码

  /***
     * 获取url 指定name的value;
     * @param url
     * @param name
     * @return
     */
    public static String getValueByName(String url, String name) {
        String result = "";
        int index = url.indexOf("?");
        String temp = url.substring(index + 1);
        String[] keyValue = temp.split("&");
        for (String str : keyValue) {
            if (str.contains(name)) {
                result = str.replace(name + "=", "");
                break;
            }
        }
        return result;
    }

正确的做法是

    val uri = Uri.parse(url)
    val valueByName = uri.getQueryParameter(key)

在apk《安全检查记录》中,我看到了这样的整改要求

名称 描述
漏洞名称 调试日志函数调用风险
漏洞描述 在APP的开发过程中,为了方便调试,通常会使用log函数输出一些信息,这会让攻击者更加容易了解APP内部结构,方便破解和攻击,甚至有可能直接获取到有价值的隐私敏感信息。

这样的日志打印,你可以删除你项目的相关代码,但是你无法删除sdk 中,因此正确的做法如下:

  • 在proguard-rules.pro 中添加如下内容
-assumenosideeffects class android.util.Log {
    public static int v(...);
    public static int i(...);
    public static int w(...);
    public static int d(...);
    public static int e(...);
}
  • 如果 proguard-rules.rpo 中有 -dontoptimize 需要特别的注意,这个东西表示不开启优化

记录一次页面卡顿问题

当主界面愈发复杂的时候,发现主界面及其卡顿,在排除自身的各种问题后,发现问题时 神策skd 造成的。

大概是 ViewTreeStatusObservable.traverse()

他们的逻辑是,大概 100ms 就会遍历一遍所有的View 然后上报,这个属于全埋点,最终反馈给神策,神策去解决。记录一下。

简单的图片圆角剪切

如果是剪切四个圆角,那么可以这样操作,下面是关键代码

private val  bitmap by lazy {
    BitmapFactory.decodeResource(
        BaseApplication.mContext.resources,
        R.mipmap.bg_hot_rank
    )
}

private val paint by lazy {
    Paint().apply {
        isAntiAlias = true //抗锯齿
       isDither = true //抖动,不同屏幕尺的使用保证图片质量
        ///位图渲染器
        shader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
    }
}

 canvas.drawRoundRect(rectF, roundAngle, roundAngle, paint)

对于,图片只有上面是圆角,下面是直角的情况, 一般的想到的是,PorterDuff.Mode

https://developer.android.com/reference/android/graphics/PorterDuff.Mode.html

其实更为简单灵活的方式是通过构建 path 的方式

   private val bitmap = BitmapFactory.decodeResource(
        BaseApplication.mContext.resources,
        R.drawable.bg_hot_rank_top
    )
    private val paintSrc = Paint().apply {
        isAntiAlias = true //抗锯齿
        isDither = true //抖动,不同屏幕尺的使用保证图片质量
        shader = BitmapShader(bitmap, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
    }

// 关键代码
  override fun draw(canvas: Canvas) {
       path.reset()
        path.addRoundRect(
            topRect,
            floatArrayOf(radius, radius, radius, radius, 0f, 0f, 0f, 0f),
            Path.Direction.CCW
        )
        canvas.drawPath(path, paintSrc)

    }

颜色的透明度

当给的颜色值是 “#ff6f”,需要获取这种颜色的 不同 透明度时,可以这样处理

 private fun getAlphaColor(color: Int, alpha: Int) = (color.and(16777215).or(alpha.shl(24)))

// 代码

val color =   Color.parseColor("#FF6F6F")

// 26 对应的是颜色不透明度 10% ,16 进制是 1a
val appha = getAlphaColor(color,26)

webview 的坑爹

公司的演示大屏,四个金刚位进入不了(查企业,查专利,查政策,产学研)无法进入,让我去排查问题。

先看了下系统的版本,7.0 预感大事不妙,这个位置使用了离线包,刚开始认为是由于网络波动,下载的离线包出了问题。

离线包的下载没有进行md5的文件完整性校验 但是切换网络等操作后,依然如此......

准备调试吧。。。。

等等,怎么线上的链接也都打不开,我突然想到 是系统的webview问题,最后一看果然是。

之前不出现问题是因为用的 tbs,他们今天反复的重装 APP,导致腾讯关闭了tbs的主动下发流程。问题知道了,然后 在线安装 tbs内核。

防不胜防

关于截图功能的坑逼

坑就坑在产品的设计上,产品的要求是发生了截屏,然后弹出分享的界面,问题就在这里,我虽然知道了长截屏的发生,但是因为上面已经有个分享界面了,长截屏的页面是分享页面,而不是用户希望的截屏页面。这是一个无解的问题,需要重新设计这个功能。

关于动画的坑

如果使用了Android 提供的动画框架,那么当系统关闭动画后,会导致动画不正常。

案例:公司的大屏幕,运营人员给我说,动画不正常,看了一会儿也没看出什么问题,最终,想了很久才明白是因为在系统设置中,关于了动画。

RecyclerView 的瀑布流布局,高度计算问题解决

itemAnimator = null

即不使用动画。

对于 ItemDecoration 有时候不生效的问题,使用 invalidateItemDecorations 进行重绘

Apk签名验证不通过

这是一个非常低级的错误,由于是插件化开发,每一个插件都有自己的签名,导致签名混乱,主包使用了其它的签名,导致签名校验失败。

这是一个非常低级的错误,确花费了大量的时间去检查代码,浪费精力。

Animator 的 onCancle 在 Android 7.0 上的不同点

在Android 7.0版本中,onCancle 调用后,会接着调用 onEnd;但是7.0以上的版本不会,需要特别注意.

七鱼云 8.5 版本

在这个版本中,AndroiManifest.xml 中有如下的代码:

 <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
        android:maxSdkVersion="29" />

导致了:

ontextCompat.checkSelfPermission(
                    requireContext(),
                    permission
                ) != PackageManager.PERMISSION_GRANTED

一直是 false 后来仔细的排查才发现问题。

在 8.5.1 中他们又删除了这行代码。离谱