一,Webveiw加载人脸认证,会提示无android.webkit.resource.VIDEO_CAPTURE权限的崩溃。后来查相关api,该权限属于android.webkit.PermissionRequest里面的一个web资源访问权限,官方定义如下:
该类定义了一个权限请求,并在Web内容请求访问受保护资源时使用。 许可请求相关事件通过
onPermissionRequest(PermissionRequest)
和onPermissionRequestCanceled(PermissionRequest)
。 必须在UI线程中调用grant()
或deny()
才能响应请求。 未来版本的WebView中可能会请求新名称未在此处定义的受保护资源,即使在较旧的Android版本上运行时也是如此。 为避免无意中授予对新权限的请求,您应将想要授予的特定权限传递给grant()
意思就是我们在用webview调起手机硬件功能(相机,录制音视频等),需要先授予相关权限,才能调起webview里面的资源,示例代码:
new WebChromeClient() {
@Override
public void onPermissionRequest(PermissionRequest request) {
//super.onPermissionRequest(request);
runOnUiThread(new Runnable() {
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public void run() {
request.grant(request.getResources());
}
});
}
};
注意:
super.onPermissionRequest(request);要注释掉
runOnUiThread 要在UI线程中授予权限
二,android各个版本主要改变适配,android每一次更新都在不断减少用户控制机器的权限,从而来提高产品的用户体验。各应用市场也在对应的更新规则,来限制不符合规则的应用上架,来保持市场的生态化,纯净化。
2.1 andorid6以下,用户权限相对较大,所以开发板基本都基于android5来生产
2.2 android6 (API23)开始,开始对权限管控,对于敏感权限需要动态申请(相机,存储等权限),普通权限可以只在清单文件配置即可(网络等权限)
public class MainActivity extends AppCompatActivity {
private static final int REQUEST_CODE = 100;
//读写文件权限
private static String[] PERMISSIONS_STORAGE = {"android.permission.WRITE_EXTERNAL_STORAGE",
"android.permission.READ_EXTERNAL_STORAGE"};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
//android6以上需要动态申请
if (Build.VERSION.SDK_INT >= 23) {
checkPermission();
}
}
private void checkPermission() {
//权限是否授权存储权限
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
//申请权限
ActivityCompat.requestPermissions(this, PERMISSIONS_STORAGE, REQUEST_CODE);
} else {
//已授权存储权限,做其它事情
}
}
@Override
public void onRequestPermissionsResult(int requestCode,
String permissions[], int[] grantResults) {
switch (requestCode) {
//权限的申请结果
case REQUEST_CODE: {
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {
//授权成功
Toast.makeText(this, "存储权限授权成功!", Toast.LENGTH_SHORT).show();
} else {
//授权失败
Toast.makeText(this, "存储权限授权失败!", Toast.LENGTH_SHORT).show();
}
}
}
}
}
2.3 android7(API24)
应用间共享文件限制 需要适配FileProvider,否则应用会崩溃, file:// URI 会触发 FileUriExposedException异常
第一步在app模块的AndroidManifest.xml里面新增provider节点
provider字段含义如下:
1、android:authorities 标识ContentProvider的唯一性,可以自己任意定义,最好是全局唯一的。
2、android:name 是指之前定义的FileProvider 子类。
3、android:exported=“false” 限制其他应用获取Provider。
4、android:grantUriPermissions=“true” 授予其它应用访问Uri权限。
5、meta-data 囊括了别名应用表。
5.1、android:name 这个值是固定的,表示要解析file_path。
5.2、android:resource 自己定义实现的映射表
第二步在app模块的res文件夹下,新建一个xml文件夹,名字就是上面 android:resource=”@xml/file_paths”对应的内容
2.4 android8(API26)
所有的通知都需要提供通知渠道Notification Channels,否则,所有通知在8.0系统上都不能正常显示
public void showNotification(Context context, String tittle, String content, Bitmap bitmap, String extras) {
Intent intent = new Intent(context, OpenClickActivity.class);
intent.putExtra("customerMessage", extras);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 1, intent, 0);
NotificationManager notificationManager = (NotificationManager) context.getSystemService(context.NOTIFICATION_SERVICE);
Notification.Builder builder;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
// create android channel
NotificationChannel androidChannel = new NotificationChannel(ANDROID_CHANNEL_ID,
ANDROID_CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH);
notificationManager.createNotificationChannel(androidChannel);
builder = new Notification.Builder(context, ANDROID_CHANNEL_ID);
} else {
builder = new Notification.Builder(context);
}
Notification notification = builder
/**设置通知左边的图标**/
.setSmallIcon(R.mipmap.juailog)
/**设置通知右边的小图标**/
.setLargeIcon(bitmap)
/**通知首次出现在通知栏,带上升动画效果的**/
.setTicker("通知来了")
/**设置通知的标题**/
.setContentTitle(tittle)
/**设置通知的内容**/
.setContentText(content)
/**通知产生的时间,会在通知信息里显示**/
.setWhen(System.currentTimeMillis())
/**设置该通知优先级**/
.setPriority(Notification.PRIORITY_DEFAULT)
/**设置这个标志当用户单击面板就可以让通知将自动取消**/
.setAutoCancel(true)
/**设置他为一个正在进行的通知。他们通常是用来表示一个后台任务,用户积极参与(如播放音乐)或以某种方式正在等待,因此占用设备(如一个文件下载,同步操作,主动网络连接)**/
.setOngoing(false)
/**向通知添加声音、闪灯和振动效果的最简单、最一致的方式是使用当前的用户默认设置,使用defaults属性,可以组合:**/
.setDefaults(Notification.DEFAULT_VIBRATE | Notification.DEFAULT_SOUND)
.setContentIntent(pendingIntent)
.build();
/**发起通知**/
int id = (int) (System.currentTimeMillis() / 1000);
notificationManager.notify(id, notification);
}
不允许后台应用启动后台服务,需要使用 startForegroundService 指定为前台服务,否则系统会停止 Service 并抛出异常
Intent intent =new Intent(this, MyService.class)
//判断android版本大于8
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
startForegroundService(intent)
} else {
startService(intent)
}
2.5 android9(API28)
限制了 HTTP 网络请求,无法正常发出请求,如果我们需要使用 HTTP 请求的话,有两种方法
方法一:需要在清单文件配置HTTP明文支持
方法二:或者也可以指定域名,在 res 的 xml 目录下新建文件 network_config.xml
2.6 Android10(API29)
用户存储权限的变更,分区存储,android在外部存储设备中为每个应用提供了一个"隔离存储沙盒",其他应用都无法直接访问您应用的沙盒文件。
内部应用私有目录:不需要申请权限,只能本应用内访问
context.getCacheDir(); /data/data/包名/cache
context.getFilesDir(); /data/data/包名/files
外部存储目录:不但需要存储权限,还需要所有文件管理权限
Environment.getExternalStorageDirectory() /storage/emulated/0
外部公共目录:只需要存储权限
/storage/emulated/0/DCIM, 另外还有MOVIE/MUSIC等很多种标准路径 Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM); /storage/emulated/0/DCIM
访问文件的位置 | 所需权限 | 访问方法 |
---|---|---|
应用的私有目录 | 无需权限即可访问 | getExternalFilesDir() |
其他应用的私有目录 | 无,但目标文件要被其应用使用FileProvider标记为可共享文件 | 通过ParcelFileDescriptor |
媒体文件目录(音频、照片、视频文件) | READ_EXTERNAL_STORAGE 仅当访问其他应用的文件时需要) |
MediaStore API |
下载目录 | 无需权限即可访问 | 存储访问框架SAF |
2.7 Android11(API30) 强制分区存储
Android10(api29)可以使用下方代码标记使用旧版存储方式
Android11(api30)强制使用Scoped Storage (分区存储) 存储方式。
2.8 Android12(API31)
后台启动 Activity 的限制,应用处于后台时,无法启动Activity。比如启动页,广告页倒计时结束可能会从后台启动activity,这种情况google市场可能拒绝该应用上架
谷歌的建议是,在后台时,可以通过创建通知的方式,向用户提供信息。由用户通过点击通知的方式,来启动 Activity,而不是直接启动。
如果有必要,还可以通过 setFullScreenIntent() 来强调这是一个立即需要处理的通知。
可以参考Android8中的通知适配来处理后台启动activity的通知
2.9 从6.0到13主要更新适配概览:
Android 6:运行时权限动态申请
Android 7:禁止向你的应用外公开 file:// URI,若要共享文件,使用FileProvider类
Android 8:引入了通知渠道,不允许后台应用启动后台服务,需要通过startForegroundService()指定为前台服务,应用有五秒的时间来调用该 Service 的 startForeground() 方法以显示可见通知。
Android 9:网络请求中,要求使用https、刘海屏API支持
Android 10:定位权限、分区存储、后台启动 Activity 的限制、深色主题
Android 11:存储机制更新、权限更新、权限更新、软件包可见性、前台服务类型、消息框的更新Android 12:新的应用启动页,声明exported,精确闹钟SCHEDULE_EXACT_ALARM权限:精确位置权限,禁止从后台启动前台服务:搜索蓝牙不再需要位置权限
Android 13:细分媒体权限,WebView废弃setAppCacheEnabled与setForceDark方法,注册静态广播要设置的可见性,新增通知权限POST_NOTIFICATIONS,新增Wi-Fi运行时权限NEARBY_WIFI_DEVICES,新增 身体传感器后台权限BODY_SENSORS_BACKGROUND,剪切板内容隐藏API,非 SDK 接口的限制
三,google市场被拒原因
3.1 从2021年开始,google及国内应用市场对无告知用户的前提下对收集用户信息的管控非常严格,设涉及到用户隐私的功能必须明确告知用户我要收集这些信息的用途,让用户选择是否同意app收集相关信息,包括但不限于位置,Mac地址,设备id等个人信息。
解决方法:
第一步:在用户第一次安装和打开app时候必须先以弹框的形式向用户展示将要获取哪些权限和信息,用户确定同意这些信息,才能进入app首页并使用其功能。同时在应用设置里面必须有二次查看用户协议和隐私政策的地方
第二步:在用户登录的时候也必须先让用户查看并勾选注册协议和隐私协议,用户勾选则代表用户同意这些协议。
第三步:涉及到推荐,兴趣这些敏感的广告内容,必须让用户可以关闭这些推荐。通常是在设置里面添加通道可以设置这些内容的显示和关闭,不推荐这些内容。
3.2 网络安全和webview的漏洞被拒,2021年开始google禁止接口协议使用http,只能使用安全协议的https
解决方法:
第一步:需要添加https支持
第二部:处理WebView SSL的 错误程序,已提醒的方式检查证书的有效性,示例:
class myWebclient extends WebViewClient {
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
final AlertDialog.Builder builder = new AlertDialog.Builder(view.getContext());
String message = "SSL Certificate error.";
switch (error.getPrimaryError()) {
case SslError.SSL_UNTRUSTED:
message = "The certificate authority is not trusted.";
break;
case SslError.SSL_EXPIRED:
message = "The certificate has expired.";
break;
case SslError.SSL_IDMISMATCH:
message = "The certificate Hostname mismatch.";
break;
case SslError.SSL_NOTYETVALID:
message = "The certificate is not yet valid.";
break;
case SslError.SSL_DATE_INVALID:
message = "The date of the certificate is invalid";
break;
case SslError.SSL_INVALID:
default:
message = "A generic error occurred";
break;
}
message += " Do you want to continue anyway?";
builder.setTitle("SSL Certificate Error");
builder.setMessage(message);
builder.setPositiveButton("continue", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
handler.proceed();
}
});
builder.setNegativeButton("cancel", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
handler.cancel();
}
});
final AlertDialog dialog = builder.create();
dialog.show();
}
}
3.3 2022年开始google禁止从后台启动activit的应用,这类应用截至2023年已被下架停止。
解决方法:
第二部分有提到android10和android11的后台启动activity适配方法,以通知的方式提醒用户后台启动activity。或者后台状态下禁止activity跳转,做拦截处理,等应用恢复到前台再去重新跳转;
判断前后台状态的示例:
public class AppFrontBackHelper {
private OnAppStatusListener mOnAppStatusListener;
public AppFrontBackHelper() {
}
/**
* 注册状态监听,仅在Application中使用
*
* @param application
* @param listener
*/
public void register(Application application, OnAppStatusListener listener) {
mOnAppStatusListener = listener;
application.registerActivityLifecycleCallbacks(activityLifecycleCallbacks);
}
public void unRegister(Application application) {
application.unregisterActivityLifecycleCallbacks(activityLifecycleCallbacks);
}
private Application.ActivityLifecycleCallbacks activityLifecycleCallbacks = new Application.ActivityLifecycleCallbacks() {
//打开的Activity数量统计
private int activityStartCount = 0;
@Override
public void onActivityCreated(Activity activity, Bundle savedInstanceState) {
}
@Override
public void onActivityStarted(Activity activity) {
activityStartCount++;
//数值从0变到1说明是从后台切到前台
if (activityStartCount == 1) {
//从后台切到前台
if (mOnAppStatusListener != null) {
mOnAppStatusListener.onFront();
}
}
}
@Override
public void onActivityResumed(Activity activity) {
}
@Override
public void onActivityPaused(Activity activity) {
}
@Override
public void onActivityStopped(Activity activity) {
activityStartCount--;
//数值从1到0说明是从前台切到后台
if (activityStartCount == 0) {
//从前台切到后台
if (mOnAppStatusListener != null) {
mOnAppStatusListener.onBack();
}
}
}
@Override
public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
}
@Override
public void onActivityDestroyed(Activity activity) {
}
};
public interface OnAppStatusListener {
void onFront();
void onBack();
}
}
使用:
//监听前后端状态
AppFrontBackHelper helper = new AppFrontBackHelper();
helper.register(this, new AppFrontBackHelper.OnAppStatusListener() {
@Override
public void onFront() {
//应用切到前台处理
Log.e("aaa", "前台");
Consts.ISBACKPROCESS=false;
}
@Override
public void onBack() {
//应用切到后台处理
Log.e("aaa", "后台");
Consts.ISBACKPROCESS = true;
}
});
3.4 2021.8以后新上架的应用必须上架aab格式的包,apk包不再支持上传。
但又出现一个问题,上架aab时老提示app bundle 签名无效
解决方法:
1,非常重要,打包前项目build.gradle ,一定不要配置签名,否则google不认你的签名
2,打包选择aab打包方式就可以了
3,上架选择google签名计划,让Google管理签名
4,然后就可以传aab包了,补充完相关信息
3.5 android.permission.REQUEST_INSTALL_PACKAGES 允许安装三方软件权限引起的被拒
谷歌2021年开始已禁止从其它方式安装app,只能从谷歌市场更新应用,所以禁止清单文件禁止配置EQUEST_INSTALL_PACKAGES权限
google回复的被拒原因:
这样只能去掉该权限,如果强烈要求内部更新,可以通过其它方案,毕竟这个权限只是限制自动安装用的
方案一:点击更新直接跳转手机内置浏览器,通过三方浏览器下载和安装,毕竟google只能限制自己应用,不能限制其它应用。
方案二:既然不能自动安装,那就手动安装,还是应用内下载升级包到内存文件夹,然后手动找到对应的升级包点击安装。
这样为了保证用户能更新到,可以两个方案结合起来,判断有浏览器就跳浏览器下载更新,否则通过下载到内存后手动更新
//跳系统浏览器更新
private void launchAppBrowser(String url) {
try {
Intent intent= new Intent();
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setAction("android.intent.action.VIEW");
intent.setData(Uri.parse(url));
activity.startActivity(intent);
dismiss();
}catch (Exception e){
//浏览器不存在
downFaceFiles(url);
}
}
//下载升级包到公共下载目录
private void downFaceFiles(String downPath) {
String folder =
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
.getAbsolutePath()+ File.separator + "lensun";
String mSinglePath = folder + File.separator + System.currentTimeMillis() + suffix;
FileDownloader.getImpl().create(downPath).setPath(mSinglePath, false)
}
当然如果google已经上架,可以跳转google市场更新app,上面方案是万一google下架了应用,还有别的渠道可以更新
//跳转Google市场
public void launchAppGoogle() {
Intent intent2 = new Intent(Intent.ACTION_VIEW);
intent2.setData(Uri.parse("https://play.google.com/store/apps/details?id=" +
activity.getPackageName()));
activity.startActivity(intent2);
}
四,TextView单词换行问题
出现的问题,一行右边空间很多,但遇到了长数字或者单词就自动换行,造成界面非常不整齐
修正后,可以正常换行,不会在根据单词数据折行
方法:有网上说 xml里面可以设置android:breakStrategy="simple"属性,但经过实践并不能解决问题,那就自己写一个控件解决吧
public class AlignTextView extends android.support.v7.widget.AppCompatTextView {
public AlignTextView(Context context) {
super(context);
}
public AlignTextView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
protected void onDraw(Canvas canvas) {
// 获取用于显示当前文本的布局
Layout layout = getLayout();
if (layout == null) return;
final int lineCount = layout.getLineCount();
if (lineCount < 2) {
//想只有一行 则不需要转化
super.onDraw(canvas);
return;
}
Paint.FontMetrics fm = getPaint().getFontMetrics();
int textHeight = (int) (Math.ceil(fm.descent - fm.ascent));
textHeight = (int) (textHeight * layout.getSpacingMultiplier() + layout.getSpacingAdd());
measureText(getMeasuredWidth(), getText(), textHeight, canvas);
}
/**
* 计算一行 显示的文字
*
* @param width 文本的宽度
* @param text//文本内容
* @param textHeight 文本大小
*/
public void measureText(int width, CharSequence text, int textHeight, Canvas canvas) {
TextPaint paint = getPaint();
paint.setColor(getCurrentTextColor());
paint.drawableState = getDrawableState();
float textWidth = StaticLayout.getDesiredWidth(text, paint);
int textLength = text.length();
float textSize = paint.getTextSize();
if (textWidth < width) canvas.drawText(text, 0, textLength, 0, textSize, paint); //不需要换行
else {
//需要换行
CharSequence lineOne = getOneLine(width, text, paint);
int lineOneNum = lineOne.length();
canvas.drawText(lineOne, 0, lineOneNum, 0, textSize, paint);
//画第二行
if (lineOneNum < textLength) {
CharSequence lineTwo = text.subSequence(lineOneNum, textLength);
lineTwo = getTwoLine(width, lineTwo, paint);
canvas.drawText(lineTwo, 0, lineTwo.length(), 0, textSize + textHeight, paint);
}
}
}
public CharSequence getTwoLine(int width, CharSequence lineTwo, TextPaint paint) {
int length = lineTwo.length();
String ellipsis = "...";
float ellipsisWidth = StaticLayout.getDesiredWidth(ellipsis, paint);
for (int i = 0; i < length; i++) {
CharSequence cha = lineTwo.subSequence(0, i);
float textWidth = StaticLayout.getDesiredWidth(cha, paint);
if (textWidth + ellipsisWidth > width) {//需要显示 ...
lineTwo = lineTwo.subSequence(0, i - 1) + ellipsis;
return lineTwo;
}
}
return lineTwo;
}
/**
* 获取第一行 显示的文本
*
* @param width 控件宽度
* @param text 文本
* @param paint 画笔
* @return
*/
public CharSequence getOneLine(int width, CharSequence text, TextPaint paint) {
CharSequence lineOne = null;
int length = text.length();
for (int i = 0; i < length; i++) {
lineOne = text.subSequence(0, i);
float textWidth = StaticLayout.getDesiredWidth(lineOne, paint);
if (textWidth >= width) {
CharSequence lastWorld = text.subSequence(i - 1, i);//最后一个字符
float lastWidth = StaticLayout.getDesiredWidth(lastWorld, paint);//最后一个字符的宽度
if (textWidth - width < lastWidth) {//不够显示一个字符 //需要缩放
lineOne = text.subSequence(0, i - 1);
}
return lineOne;
}
}
return lineOne;
}
}
五,jar包依赖冲突
5.1 使用gradle 命令分析 依赖关系
查看全部依赖
./gradlew [module]:dependencies
比如 ./gradlew app:dependencies
只查看 implementation 的依赖树
./gradlew [module]:dependencies --configuration [variants 类型]
比如:./gradlew :app:dependencies --configuration implementation
查看指定库的依赖
./gradlew :app:dependencyInsight --dependency [指定库] --configuration compile
比如:./gradlew :app:dependencyInsight --dependency fastjson --configuration compile
5.2 Task工具分析
gradle -> app -> tasks -> help -> dependencies
我们以glide为例,先添加一个
可以看到只有一个glide依赖
+--- project :multidexapplication (*)
\--- com.github.bumptech.glide:glide:4.11.0
+--- com.github.bumptech.glide:gifdecoder:4.11.0
| \--- androidx.annotation:annotation:1.0.0
+--- com.github.bumptech.glide:disklrucache:4.11.0
+--- com.github.bumptech.glide:annotations:4.11.0
+--- androidx.fragment:fragment:1.0.0
| +--- androidx.core:core:1.0.0
| | +--- androidx.annotation:annotation:1.0.0
| | +--- androidx.collection:collection:1.0.0
| | | \--- androidx.annotation:annotation:1.0.0
| | +--- androidx.lifecycle:lifecycle-runtime:2.0.0
| | | +--- androidx.lifecycle:lifecycle-common:2.0.0
| | | | \--- androidx.annotation:annotation:1.0.0
| | | +--- androidx.arch.core:core-common:2.0.0
| | | | \--- androidx.annotation:annotation:1.0.0
| | | \--- androidx.annotation:annotation:1.0.0
| | \--- androidx.versionedparcelable:versionedparcelable:1.0.0
| | +--- androidx.annotation:annotation:1.0.0
| | \--- androidx.collection:collection:1.0.0 (*)
| +--- androidx.legacy:legacy-support-core-ui:1.0.0
| | +--- androidx.annotation:annotation:1.0.0
| | +--- androidx.core:core:1.0.0 (*)
| | +--- androidx.legacy:legacy-support-core-utils:1.0.0
| | | +--- androidx.annotation:annotation:1.0.0
| | | +--- androidx.core:core:1.0.0 (*)
| | | +--- androidx.documentfile:documentfile:1.0.0
| | | | \--- androidx.annotation:annotation:1.0.0
| | | +--- androidx.loader:loader:1.0.0
| | | | +--- androidx.annotation:annotation:1.0.0
| | | | +--- androidx.core:core:1.0.0 (*)
| | | | +--- androidx.lifecycle:lifecycle-livedata:2.0.0
| | | | | +--- androidx.arch.core:core-runtime:2.0.0
| | | | | | +--- androidx.annotation:annotation:1.0.0
| | | | | | \--- androidx.arch.core:core-common:2.0.0 (*)
| | | | | +--- androidx.lifecycle:lifecycle-livedata-core:2.0.0
| | | | | | +--- androidx.lifecycle:lifecycle-common:2.0.0 (*)
| | | | | | +--- androidx.arch.core:core-common:2.0.0 (*)
| | | | | | \--- androidx.arch.core:core-runtime:2.0.0 (*)
| | | | | \--- androidx.arch.core:core-common:2.0.0 (*)
| | | | \--- androidx.lifecycle:lifecycle-viewmodel:2.0.0
| | | | \--- androidx.annotation:annotation:1.0.0
| | | +--- androidx.localbroadcastmanager:localbroadcastmanager:1.0.0
| | | | \--- androidx.annotation:annotation:1.0.0
| | | \--- androidx.print:print:1.0.0
| | | \--- androidx.annotation:annotation:1.0.0
| | +--- androidx.customview:customview:1.0.0
| | | +--- androidx.annotation:annotation:1.0.0
| | | \--- androidx.core:core:1.0.0 (*)
| | +--- androidx.viewpager:viewpager:1.0.0
| | | +--- androidx.annotation:annotation:1.0.0
| | | +--- androidx.core:core:1.0.0 (*)
| | | \--- androidx.customview:customview:1.0.0 (*)
| | +--- androidx.coordinatorlayout:coordinatorlayout:1.0.0
| | | +--- androidx.annotation:annotation:1.0.0
| | | +--- androidx.core:core:1.0.0 (*)
| | | \--- androidx.customview:customview:1.0.0 (*)
| | +--- androidx.drawerlayout:drawerlayout:1.0.0
| | | +--- androidx.annotation:annotation:1.0.0
| | | +--- androidx.core:core:1.0.0 (*)
| | | \--- androidx.customview:customview:1.0.0 (*)
| | +--- androidx.slidingpanelayout:slidingpanelayout:1.0.0
| | | +--- androidx.annotation:annotation:1.0.0
| | | +--- androidx.core:core:1.0.0 (*)
| | | \--- androidx.customview:customview:1.0.0 (*)
| | +--- androidx.interpolator:interpolator:1.0.0
| | | \--- androidx.annotation:annotation:1.0.0
| | +--- androidx.swiperefreshlayout:swiperefreshlayout:1.0.0
| | | +--- androidx.annotation:annotation:1.0.0
| | | +--- androidx.core:core:1.0.0 (*)
| | | \--- androidx.interpolator:interpolator:1.0.0 (*)
| | +--- androidx.asynclayoutinflater:asynclayoutinflater:1.0.0
| | | +--- androidx.annotation:annotation:1.0.0
| | | \--- androidx.core:core:1.0.0 (*)
| | \--- androidx.cursoradapter:cursoradapter:1.0.0
| | \--- androidx.annotation:annotation:1.0.0
| +--- androidx.legacy:legacy-support-core-utils:1.0.0 (*)
| +--- androidx.annotation:annotation:1.0.0
| +--- androidx.loader:loader:1.0.0 (*)
| \--- androidx.lifecycle:lifecycle-viewmodel:2.0.0 (*)
+--- androidx.vectordrawable:vectordrawable-animated:1.0.0
| +--- androidx.vectordrawable:vectordrawable:1.0.0
| | +--- androidx.annotation:annotation:1.0.0
| | \--- androidx.core:core:1.0.0 (*)
| \--- androidx.legacy:legacy-support-core-ui:1.0.0 (*)
\--- androidx.exifinterface:exifinterface:1.0.0
\--- androidx.annotation:annotation:1.0.0
我们再添加个不同版本的glide
可以看到在末尾多出了个glide的依赖冲突
+--- project :multidexapplication (*)
+--- com.github.bumptech.glide:glide:4.11.0
| +--- com.github.bumptech.glide:gifdecoder:4.11.0
| | \--- androidx.annotation:annotation:1.0.0
| +--- com.github.bumptech.glide:disklrucache:4.11.0
| +--- com.github.bumptech.glide:annotations:4.11.0
| +--- androidx.fragment:fragment:1.0.0
| | +--- androidx.core:core:1.0.0
| | | +--- androidx.annotation:annotation:1.0.0
| | | +--- androidx.collection:collection:1.0.0
| | | | \--- androidx.annotation:annotation:1.0.0
| | | +--- androidx.lifecycle:lifecycle-runtime:2.0.0
| | | | +--- androidx.lifecycle:lifecycle-common:2.0.0
| | | | | \--- androidx.annotation:annotation:1.0.0
| | | | +--- androidx.arch.core:core-common:2.0.0
| | | | | \--- androidx.annotation:annotation:1.0.0
| | | | \--- androidx.annotation:annotation:1.0.0
| | | \--- androidx.versionedparcelable:versionedparcelable:1.0.0
| | | +--- androidx.annotation:annotation:1.0.0
| | | \--- androidx.collection:collection:1.0.0 (*)
| | +--- androidx.legacy:legacy-support-core-ui:1.0.0
| | | +--- androidx.annotation:annotation:1.0.0
| | | +--- androidx.core:core:1.0.0 (*)
| | | +--- androidx.legacy:legacy-support-core-utils:1.0.0
| | | | +--- androidx.annotation:annotation:1.0.0
| | | | +--- androidx.core:core:1.0.0 (*)
| | | | +--- androidx.documentfile:documentfile:1.0.0
| | | | | \--- androidx.annotation:annotation:1.0.0
| | | | +--- androidx.loader:loader:1.0.0
| | | | | +--- androidx.annotation:annotation:1.0.0
| | | | | +--- androidx.core:core:1.0.0 (*)
| | | | | +--- androidx.lifecycle:lifecycle-livedata:2.0.0
| | | | | | +--- androidx.arch.core:core-runtime:2.0.0
| | | | | | | +--- androidx.annotation:annotation:1.0.0
| | | | | | | \--- androidx.arch.core:core-common:2.0.0 (*)
| | | | | | +--- androidx.lifecycle:lifecycle-livedata-core:2.0.0
| | | | | | | +--- androidx.lifecycle:lifecycle-common:2.0.0 (*)
| | | | | | | +--- androidx.arch.core:core-common:2.0.0 (*)
| | | | | | | \--- androidx.arch.core:core-runtime:2.0.0 (*)
| | | | | | \--- androidx.arch.core:core-common:2.0.0 (*)
| | | | | \--- androidx.lifecycle:lifecycle-viewmodel:2.0.0
| | | | | \--- androidx.annotation:annotation:1.0.0
| | | | +--- androidx.localbroadcastmanager:localbroadcastmanager:1.0.0
| | | | | \--- androidx.annotation:annotation:1.0.0
| | | | \--- androidx.print:print:1.0.0
| | | | \--- androidx.annotation:annotation:1.0.0
| | | +--- androidx.customview:customview:1.0.0
| | | | +--- androidx.annotation:annotation:1.0.0
| | | | \--- androidx.core:core:1.0.0 (*)
| | | +--- androidx.viewpager:viewpager:1.0.0
| | | | +--- androidx.annotation:annotation:1.0.0
| | | | +--- androidx.core:core:1.0.0 (*)
| | | | \--- androidx.customview:customview:1.0.0 (*)
| | | +--- androidx.coordinatorlayout:coordinatorlayout:1.0.0
| | | | +--- androidx.annotation:annotation:1.0.0
| | | | +--- androidx.core:core:1.0.0 (*)
| | | | \--- androidx.customview:customview:1.0.0 (*)
| | | +--- androidx.drawerlayout:drawerlayout:1.0.0
| | | | +--- androidx.annotation:annotation:1.0.0
| | | | +--- androidx.core:core:1.0.0 (*)
| | | | \--- androidx.customview:customview:1.0.0 (*)
| | | +--- androidx.slidingpanelayout:slidingpanelayout:1.0.0
| | | | +--- androidx.annotation:annotation:1.0.0
| | | | +--- androidx.core:core:1.0.0 (*)
| | | | \--- androidx.customview:customview:1.0.0 (*)
| | | +--- androidx.interpolator:interpolator:1.0.0
| | | | \--- androidx.annotation:annotation:1.0.0
| | | +--- androidx.swiperefreshlayout:swiperefreshlayout:1.0.0
| | | | +--- androidx.annotation:annotation:1.0.0
| | | | +--- androidx.core:core:1.0.0 (*)
| | | | \--- androidx.interpolator:interpolator:1.0.0 (*)
| | | +--- androidx.asynclayoutinflater:asynclayoutinflater:1.0.0
| | | | +--- androidx.annotation:annotation:1.0.0
| | | | \--- androidx.core:core:1.0.0 (*)
| | | \--- androidx.cursoradapter:cursoradapter:1.0.0
| | | \--- androidx.annotation:annotation:1.0.0
| | +--- androidx.legacy:legacy-support-core-utils:1.0.0 (*)
| | +--- androidx.annotation:annotation:1.0.0
| | +--- androidx.loader:loader:1.0.0 (*)
| | \--- androidx.lifecycle:lifecycle-viewmodel:2.0.0 (*)
| +--- androidx.vectordrawable:vectordrawable-animated:1.0.0
| | +--- androidx.vectordrawable:vectordrawable:1.0.0
| | | +--- androidx.annotation:annotation:1.0.0
| | | \--- androidx.core:core:1.0.0 (*)
| | \--- androidx.legacy:legacy-support-core-ui:1.0.0 (*)
| \--- androidx.exifinterface:exifinterface:1.0.0
| \--- androidx.annotation:annotation:1.0.0
\--- com.github.bumptech.glide:glide:4.8.0 -> 4.11.0 (*)
注意:
1,带有 (*) 的依赖表示该库有多个版本,而且已被高版本覆盖
2,4.8.0 -> 4.11.0 (*) 表示会先使用高版本,如果需要使用低版本的 API ,需要排除掉高版本
5.3 解决依赖冲突方法
排除group或者组
从com.github.bumptech.glide:glide:4.11.0 依赖中排除掉 com.android.support 组,包括 supportv7,supportv4
api("com.github.bumptech.glide:glide:4.11.0") {
exclude group: 'com.android.support'
}从 com.github.bumptech.glide:glide:4.11.0 这个依赖库中排除掉 supportV4 模块
api("com.github.bumptech.glide:glide:4.11.0") {
exclude module: 'support-v4'
}
分别指定版本
configurations.all {
// 遍历所有依赖库
resolutionStrategy.eachDependency { DependencyResolveDetails details ->
def requested = details.requested
// 查到所有 support 库
if (requested.group == 'com.android.support:support-v4') {
// multidex 使用 26.0.0 ,其他使用 28.0.0 版本
if (requested.name.startsWith("glide")) {
details.useVersion '26.0.0'
}else {
details.useVersion '28.0.0'
}
}
}
}
统一指定版本,support-v4包强制使用28.0.0版本
configurations.all {
resolutionStrategy {
force 'com.android.support:support-v4:28.0.0'
}
}
5.4 实例之集成融云SDK版本不匹配,由于新版库依赖的gradle编译环境高,而且里面的库版本也高,这个时候就要排除高版本的库或者降低版本
报错信息
解决方法:排除掉有报错信息的库
implementation ('cn.rongcloud.sdk:im_kit:5.4.0')
{
exclude group: 'androidx.lifecycle', module: 'lifecycle-runtime'
exclude group: 'androidx.room', module: 'room-runtime'
}
5.5 资源冲突,aapt这种也基本上是存在不同版本的库
解决方法,定位该资源的位置,找到是哪个库冲突
可以看到是appcompat引用的多个版本,这个时候统一下版本就可以了,全部改为1.2.0版本,可以看到再编译就没这个报错了
六,保留两位小数
6.1 使用场景
需要保留小数的一般与金额有关,与金额相关的大多是商城应用,这类应用对金额要求非常严格,毕竟客户和平台谁都不愿吃亏,较真的话连一分钱的误差可能都会引起不满,所以小数位一定要处理好
6.2 实例
我们项目在运行一段时间后,出现一个退款问题,买个多件商品但可以批次售后退款,这个时候就出来了app端算出来的金额和实际金额不一样。实际支付金额是10元,但由于app端算的是单价3.33,数量3,导致最后只能最多退9.99
6.3 解决方法,产品最后给出的方案是差价补到最后一次退款的金额上,保证实付和退款金额一致
6.4 在上面基础上计算金额的方法还得和后端保持一致,是四舍五入呢还是只截取两位
常规的保留两位方法都是四舍五入的,如下面2种:
第一种,四舍五入
BigDecimal a = new BigDecimal(123456789.03);
DecimalFormat df = new DecimalFormat("##0");
System.out.println(df.format(a)));
第二种,四舍五入
BigDecimal bg = new BigDecimal(123456789.03);
double f1 = bg.setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue();
System.out.println(f1);
第三种,不四舍五入,这种在使用中比较奇怪,大多数都没问题,只是个别遇到小数第二位3,会变为2,比如123456789.03
DecimalFormat formater = new DecimalFormat();
formater.setMaximumFractionDigits(2);
formater.setGroupingSize(0);
formater.setRoundingMode(RoundingMode.FLOOR);
System.out.println(formater.format(123456789.03));
第四种,不四舍五入,直接截取
BigDecimal a = new BigDecimal(123456789.03).divide(new BigDecimal(1000)).setScale(2,BigDecimal.ROUND_DOWN);
System.out.println(a);
选用哪种可以根据自己的实际情况用
6.2 涉及到加减乘除运算一定要用精准的工具计算,不然正常的计算可能会因为精度问题导致多或者少1分钱
比如:正常运算2.0-1.1=0.8999999
这就出现的精度丢失,如果最后在不让四舍五入保留两位,就会变为0.89,这肯定是不对的
所以要用到精准计算工具BigDecimal
这样计算出来就是:正常运算2.0-1.1=0.9
/**
* 加减法精确计算类
*/
public static Double add(Double parms1, Double param2) {
if (parms1 == null) {
parms1 = 0D;
}
f (param2 == null) {
param2 = 0D;
}
return new BigDecimal(String.valueOf(parms1)).add(new BigDecimal(String.valueOf(param2))).doubleValue();
}
/**
* 减法
*/
public static Double subtract(Double parms1, Double param2) {
if (parms1 == null) {
parms1 = 0D;
}
if (param2 == null) {
param2 = 0D;
}
return new BigDecimal(String.valueOf(parms1)).subtract(new BigDecimal(String.valueOf(param2))).doubleValue();
}
/**
* 乘法
*/
public static Double multiply(Double parms1, Double param2) {
if (parms1 == null) {
parms1 = 0D;
}
if (param2 == null) {
param2 = 0D;
}
return new BigDecimal(String.valueOf(parms1)).multiply(new BigDecimal(String.valueOf(param2))).doubleValue();
}
/**
* 除法
* digit:小数位数
* roundType:四舍五入或者向下取整
*/
public static Double divide(Double parms1, Double param2, int digit, int roundType) {
if (parms1 == null) {
parms1 = 0D;
}
if (param2 == null || param2 == 0) {
return 0D;
}
return new BigDecimal(String.valueOf(parms1)).divide(new BigDecimal(String.valueOf(param2)), digit, roundType).doubleValue();
}
七,GitHub,Gitee下载上传失败
可能是文件太大,或者SSL协议验证不对或者
依次尝试执行以下命令
去掉验证,或者更改验证版本
git config http.sslVerify "false"
git config http.sslVersion tlsv1.2
加大git的缓冲区
git config --global http.postBuffer 1048576000