小程序实现H5页面加载一般通过页面内使用<web-view>组件实现。有一种场景：商城H5页面展示之后点击落单需要完成实名登录，登录完成之后跳转落单页面。一般的做法是先跳转登录，完成之后，通过wx.redirectTo或wx.navigateTo等方法跳转到落单页面。事实上这种做法确实能达到效果。但是如果下一个页面（落单页面）依赖了上一个页面缓存的数据，比如缓存在sessionStorage的数据，就没办法带入到新的页面，因为新页面是新的webview不存在上一个页面缓存的数据。为了解决这样的问题，我们需要实现不新开webview也能加载页面。废话不多，代码如下。 //返回上一页 let pages = getCurrentPages(); let prevPage = pages[pages.length - 2]; if (prevPage.route.indexOf('h5container') !== -1) { //把需要回传的值保存到上一个页面 prevPage.setURL(decodeURIComponent(this._data.queryParam.targetUrl)); wx.navigateBack({ delta: 1 }) } else { wx.redirectTo({ url: `/pages/h5container/h5container?targetUrl=${this._data.queryParam.targetUrl}` }); }原理：上面的代码主要是拿到上一个H5页面的实例，然后通过调用里面的方法去重新设置页面链接url，然后返回到上一个H5页面。实现效果就是，页面刷新成了新的链接，但是没有打开新的webView页面。完美解决了上述问题。
Git命令行修改远程仓库地址--方法/实例设置仓库命令：git remote -v //查看修改前的仓库 git remote set-url origin xxx //xxx代表新的仓库地址 git remote -v //查看修改后的仓库如果执行git remote set-url origin xxx报如下错误：fatal: No such remote 'origin'说明以前还没有设置过远程仓库地址，此时需使用git remote add:git remote add origin xxx查看远程仓库：git remote -vgit init 仓库报错：Reinitialized existing Git repository in ...解决方法一、初始化git项目时，报错1、本地初始化项目cd到对应的项目路径下，打开bash。增对这里有很多很多的操作，例如cmd,ps,cmder,bash等执行git init2、报如下错误Reinitialized existing Git repository in…二、报错原因该路径中已经存在.git文件了三、解决方法1、可以在当前文件下的控制台中输入 ls-a 查看，有.git 的话，使用 rm -rf .git 删除之后重新初始化就可以了2、已经存在.git文件，说明可以直接使用git命令，使用上面的设置远程仓库命令或修改其他公用git参数即可使用。xcrun: error: invalid active developer path解决办法mac下如果安装过xcode,之后又卸载，再使用git等命令时就提示错误。invalid active path(Applications/Xcode.app/Contents/Developer)如果不想安装xcode,那么可以通过重置系统默认开发工具路径.可以通过xcode-select命令来重置系统默认的CommandLineTools路径，命令如下：sudo xcode-select -rsudo xcode-select --switch /Library/Developer/CommandLineToolsxcode-select -p参考链接：git--修改远程仓库的地址--方法/实例git报错：Reinitialized existing Git repository in ...解决方法
Fastjson JSONObject.toJSONString 出现 $ref: "$."解决办法（重复引用）
Fastjson JSONObject.toJSONString 出现 $ref: "$."的解决办法（重复引用）首先，fastjson作为一款序列化引擎，不可避免的会遇到循环引用的问题，为了避免StackOverflowError异常，fastjson会对引用进行检测。如果检测到存在重复/循环引用的情况，fastjson默认会以“引用标识”代替同一对象，而非继续循环解析导致StackOverflowError。解决办法1 关闭检查: JSON.toJSONString(object, SerializerFeature.DisableCircularReferenceDetect); 解决办法2: 创建新对象 不循环引用问题根本原因找到了。是因为我下面那段代码的时候，重复给map赋值了相同key的值，只是改变了值的内容。实际上，Map.get()出来的内容是持有map里的对象引用的，所以修改了对象的值之后，Map里的值自动改好了，不需要再进行Map.put();如果再做一次Map.put();相当于对同一个key插入了之前这个key的同一个引用。就会触发FastJson的循环引用保护机制。导致出现$ref: "$."当然，用JSON.toJSONString(object, SerializerFeature.DisableCircularReferenceDetect); 这个方法可以解决掉出现 $ref: “$.”的问题。但实际上，那个循环引用会导致别的key里面的内容被挤掉。最后数据还是错误的。解决方案所以根本解决办法是：不要对Map进行相同key的赋值操作。如果要修改这个key对应的value，如果value是对象，需要先get，再对对象引用进行修改，这样Map会自动修改。 //这里添加导致重复添加，map序列错乱，Fastjson编译出来的内容有问题； typeList.put(mImageNameTypeId, typeItem); Log.d(TAG + " maomao onDelete 删除照片前 fastjson typeList:" + JSON.toJSONString(typeList)); TypeItem typeItem = typeList.get(typeId); String imgNameStr = typeItem == null ? "" : typeItem.TypeValue; String[] imgNames = imgNameStr.split(","); imgNames[position] = ""; StringBuffer sb = new StringBuffer(); for (int i = 0; i < imgNames.length; i++) { if (!TextUtils.isEmpty(imgNames[i])) { sb.append(imgNames[i]).append(","); } } Log.d(TAG + " onDelete 删除照片 before imgNames：" + imgNameStr); Log.d(TAG + " onDelete 删除照片 after imgNames：" + sb.toString()); if (sb.lastIndexOf(",") >= 0 && sb.lastIndexOf(",") == sb.length() - 1) { String substring = sb.substring(0, sb.length() - 1); sb.replace(0, sb.length(), substring); } Log.d(TAG + " onDelete 删除照片 after sub string imgNames：" + sb.toString()); typeItem.TypeValue = sb.toString(); //typeList.put(mImageNameTypeId, typeItem);//这里添加导致重复添加，map序列错乱，Fastjson编译出来的内容有问题； Log.d(TAG + " maomao onDelete 删除照片后 fastjson typeList:" + JSON.toJSONString(typeList));
问题背景一个长页面内有ScrollView，也有EditText，EditText有焦点时，跳转到其他页面操完跳回当前页面，此时页面内容自动滚动到EditText所在的位置，用户体验非常不好。问题分析参考：https://www.jianshu.com/p/192f160f9e60private void scrollToChild(View child) { child.getDrawingRect(mTempRect); /* Offset from child's local coordinates to ScrollView coordinates */ offsetDescendantRectToMyCoords(child, mTempRect); int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect); if (scrollDelta != 0) { scrollBy(0, scrollDelta); } }ScrollView的自滚动方法中基本都是通过调用computeScrollDeltaToGetChildRectOnScreen方法。如果该方法返回值为0，则不滚动。解决办法自定义ScrollView，重写ScrollView的computeScrollDeltaToGetChildRectOnScreen方法，将返回值设为0。public class NoScrollFocusScrollView extends ScrollView { public NoScrollFocusScrollView(Context context) { super(context); } public NoScrollFocusScrollView(Context context, AttributeSet attrs) { super(context, attrs); } public NoScrollFocusScrollView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } @Override protected int computeScrollDeltaToGetChildRectOnScreen(Rect rect) { return 0; } }
最近遇到一个Android12版本View.post()获取不到宽高值的问题。这个问题出在创建Bitmap的时候。下面是一段生成页面图片的代码，主要是将一个布局生成为一个图片内容Bitmap。报错代码 final View waterMark = CameraPhoto.getWaterMark(activity, tempPath, waterMarkBean); if (waterMark != null) { waterMark.post(new Runnable() { @Override public void run() { CameraPhoto.saveToTemp(CameraPhoto.getViewBitmap(waterMark)); CameraPhoto.removeTempletView(activity, waterMark); threadToSave(); } }); } /** * 截取view内容，生成图片 * * @param view 容器控件 * @return 图片bitmap */ public static Bitmap getViewBitmap(View view) { try { Bitmap bitmap; // 创建相应大小的bitmap bitmap = Bitmap.createBitmap(view.getMeasuredWidth(), view.getMeasuredHeight(), Bitmap.Config.ARGB_8888); final Canvas canvas = new Canvas(bitmap); //绘制viewGroup内容 view.draw(canvas); return bitmap; } catch (Exception e) { e.printStackTrace(); return null; } } 报错原因上面代码在执行到Bitmap.createBitmap(view.getMeasuredWidth(), view.getMeasuredHeight()时，由于view.getMeasuredWidth()和view.getMeasuredHeight()值为0，导致crash。得到如下报错：java.lang.IllegalArgumentException: width and height must be > 0一般来说，在View.post()方法中获取宽高是能获取到的。 view.post(new Runnable() { @Override public void run() { //这里获取布局宽高 } }); 但是，在Android12系统中,view.post方法确实获取不到宽高。具体原因（源码）待分析。解决方案根据经验，我猜测刚inflate出来的view可能没有被加载到屏幕。为了解决这个问题，我尝试使用view.postDelayed方法，延时100毫秒获取宽高。 waterMark.postDelayed(new Runnable() { @Override public void run() { //这里获取布局宽高 } }, 100);亲测有效，问题得到解决。
客户端阿里云oss图片上传有两种形式。一种是通过搭建私有服务，客户端通过获取实时授权口令之后上传图片。另一种是通过直传的方式，无需口令，直接将图片上传到阿里云服务器。两种各有优点，第一种适合安全性私密性要求比较高的项目，第二种则适合安全性要求低，实时性要求高的项目。两种上传方式又包含同步上传和异步上传两种传输形式。下面以【直传】方式的【同步上传】为例介绍阿里云oss图片上传流程。包含普通同步上传断点续传同步上传一、准备工作准备你的Config文件import android.os.Environment; import java.io.File; public class Config { // To run the sample correctly, the following variables must have valid values. // The endpoint value below is just the example. Please use proper value according to your region // 访问的endpoint地址，与你的存储区域有关系 public static final String OSS_ENDPOINT = "http://oss-cn-shanghai.aliyuncs.com"; //callback 测试地址 public static final String OSS_CALLBACK_URL = "http://oss-demo.aliyuncs.com:23450"; // STS 鉴权服务器地址，使用前请参照文档 https://help.aliyun.com/document_detail/31920.html 介绍配置STS 鉴权服务器地址。 // 或者根据工程sts_local_server目录中本地鉴权服务脚本代码启动本地STS 鉴权服务器。详情参见sts_local_server 中的脚本内容。 public static final String STS_SERVER_URL = "http://*.*.*.*:*/sts/getsts";//STS 地址，私有服务地址，非必选 public static final String BUCKET_NAME = "bucket-spar";// 上传的仓库名 public static final String OSS_ACCESS_KEY_ID = "你的OSS_ACCESS_KEY_ID"; public static final String OSS_ACCESS_KEY_SECRET = "你的OSS_ACCESS_KEY_SECRET"; //下面是非必选 public static final int DOWNLOAD_SUC = 1; public static final int DOWNLOAD_Fail = 2; public static final int UPLOAD_SUC = 3; public static final int UPLOAD_Fail = 4; public static final int UPLOAD_PROGRESS = 5; public static final int LIST_SUC = 6; public static final int HEAD_SUC = 7; public static final int RESUMABLE_SUC = 8; public static final int SIGN_SUC = 9; public static final int BUCKET_SUC = 10; public static final int GET_STS_SUC = 11; public static final int MULTIPART_SUC = 12; public static final int STS_TOKEN_SUC = 13; public static final int FAIL = 9999; public static final int REQUESTCODE_AUTH = 10111; public static final int REQUESTCODE_LOCALPHOTOS = 10112; public static final int REQUESTCODE_OPEN_DOCUMENT = 10113; } 二、普通同步上传示例： /** * 上传方法,普通同步上传（备份。原来用的这个，后续用的断点续传） * * @param objectKey 标识 * @param path 需上传文件的路径 * @return 外网访问的路径 */ private static String uploadNormal(String objectKey, String path) { try { // 构造上传请求 PutObjectRequest request = new PutObjectRequest(BUCKET_NAME, objectKey, path); //得到client OSS client = getOSSClient(); //上传获取结果 PutObjectResult result = client.putObject(request); //获取可访问的url String url = client.presignPublicObjectURL(BUCKET_NAME, objectKey); //格式打印输出 Log.e(String.format("PublicObjectURL:%s", url)); return url; } catch (Exception e) { e.printStackTrace(); return null; } }三、断点续传同步上传官方文档：断点续传官方文档示例： /** * 上传方法，断点续传同步上传 * * @param objectKey 标识 * @param path 需上传文件的路径 * @return 外网访问的路径 */ private static String upload(String objectKey, String path) { try { String recordDirectory = Environment.getExternalStorageDirectory().getAbsolutePath() + "/oss_record/"; File recordDir = new File(recordDirectory); // 确保断点记录的保存文件夹已存在，如果不存在则新建断点记录的保存文件夹。 if (!recordDir.exists()) { recordDir.mkdirs(); } // 创建断点续传上传请求，并指定断点记录文件的保存路径，保存路径为断点记录文件的绝对路径。 ResumableUploadRequest request = new ResumableUploadRequest(BUCKET_NAME, objectKey, path, recordDirectory); // 调用OSSAsyncTask cancel()方法时，设置DeleteUploadOnCancelling为false，表示不删除断点记录文件，下次再上传同一个文件时将从断点记录处继续上传。如果不设置此参数，则默认值为true，表示删除断点记录文件，下次再上传同一个文件时则重新上传。 request.setDeleteUploadOnCancelling(false); // 设置上传回调。 request.setProgressCallback(new OSSProgressCallback<ResumableUploadRequest>() { @Override public void onProgress(ResumableUploadRequest request, long currentSize, long totalSize) { Log.d("resumableUpload currentSize: " + currentSize + " totalSize: " + totalSize); } }); //得到client OSS client = getOSSClient(); //上传获取结果 ResumableUploadResult result = client.resumableUpload(request); String url = ""; if (result != null) { url = result.getLocation();//图片在服务器的地址 } //格式打印输出 Log.e(String.format("PublicObjectURL:%s", url)); return url; } catch (Exception e) { e.printStackTrace(); return null; } } 四、图片上传工具类完整版 import android.os.Environment; import android.text.format.DateFormat; import com.alibaba.sdk.android.oss.OSS; import com.alibaba.sdk.android.oss.OSSClient; import com.alibaba.sdk.android.oss.callback.OSSProgressCallback; import com.alibaba.sdk.android.oss.common.auth.OSSCredentialProvider; import com.alibaba.sdk.android.oss.common.auth.OSSPlainTextAKSKCredentialProvider; import com.alibaba.sdk.android.oss.model.PutObjectRequest; import com.alibaba.sdk.android.oss.model.PutObjectResult; import com.alibaba.sdk.android.oss.model.ResumableUploadRequest; import com.alibaba.sdk.android.oss.model.ResumableUploadResult; import com.storecs.westore.base.Log; import com.storecs.westore.base.app.MyApp; import java.io.File; import java.util.Date; public class UploadHelper { //与个人的存储区域有关 private static final String ENDPOINT = Config.OSS_ENDPOINT; //上传仓库名 private static final String BUCKET_NAME = Config.BUCKET_NAME; private static final String ACCESS_KEY_ID = Config.OSS_ACCESS_KEY_ID; private static final String ACCESS_KEY_SECRET = Config.OSS_ACCESS_KEY_SECRET; private static OSS getOSSClient() { OSSCredentialProvider credentialProvider = new OSSPlainTextAKSKCredentialProvider(ACCESS_KEY_ID, ACCESS_KEY_SECRET); return new OSSClient(MyApp.getContext(), ENDPOINT, credentialProvider); } /** * 上传方法，断点续传同步上传 * * @param objectKey 标识 * @param path 需上传文件的路径 * @return 外网访问的路径 */ private static String upload(String objectKey, String path) { try { String recordDirectory = Environment.getExternalStorageDirectory().getAbsolutePath() + "/oss_record/"; File recordDir = new File(recordDirectory); // 确保断点记录的保存文件夹已存在，如果不存在则新建断点记录的保存文件夹。 if (!recordDir.exists()) { recordDir.mkdirs(); } // 创建断点续传上传请求，并指定断点记录文件的保存路径，保存路径为断点记录文件的绝对路径。 ResumableUploadRequest request = new ResumableUploadRequest(BUCKET_NAME, objectKey, path, recordDirectory); // 调用OSSAsyncTask cancel()方法时，设置DeleteUploadOnCancelling为false，表示不删除断点记录文件，下次再上传同一个文件时将从断点记录处继续上传。如果不设置此参数，则默认值为true，表示删除断点记录文件，下次再上传同一个文件时则重新上传。 request.setDeleteUploadOnCancelling(false); // 设置上传回调。 request.setProgressCallback(new OSSProgressCallback<ResumableUploadRequest>() { @Override public void onProgress(ResumableUploadRequest request, long currentSize, long totalSize) { Log.d("resumableUpload currentSize: " + currentSize + " totalSize: " + totalSize); } }); //得到client OSS client = getOSSClient(); //上传获取结果 ResumableUploadResult result = client.resumableUpload(request); String url = ""; if (result != null) { url = result.getLocation();//图片在服务器的地址 } //格式打印输出 Log.e(String.format("PublicObjectURL:%s", url)); return url; } catch (Exception e) { e.printStackTrace(); return null; } } /** * 上传方法,普通同步上传（备份。原来用的这个，后续用的断点续传） * * @param objectKey 标识 * @param path 需上传文件的路径 * @return 外网访问的路径 */ private static String uploadNormal(String objectKey, String path) { try { // 构造上传请求 PutObjectRequest request = new PutObjectRequest(BUCKET_NAME, objectKey, path); //得到client OSS client = getOSSClient(); //上传获取结果 PutObjectResult result = client.putObject(request); //获取可访问的url String url = client.presignPublicObjectURL(BUCKET_NAME, objectKey); //格式打印输出 Log.e(String.format("PublicObjectURL:%s", url)); return url; } catch (Exception e) { e.printStackTrace(); return null; } } /** * 上传普通图片 * * @param name 图片名称 * @param path 本地地址 * @return 服务器地址 */ public static String uploadImage(String name, String path) { String key = getObjectImageKey(name); return upload(key, path); } /** * 上传头像 * * @param name 图片名称 * @param path 本地地址 * @return 服务器地址 */ public static String uploadPortrait(String name, String path) { String key = getObjectPortraitKey(name); return upload(key, path); } /** * 上传audio * * @param path 本地地址 * @return 服务器地址 */ public static String uploadAudio(String path) { String key = getObjectAudioKey(path); return upload(key, path); } /** * 获取时间 * * @return 时间戳 例如:201805 */ private static String getDateString() { return DateFormat.format("yyyyMM", new Date()).toString(); } /** * 上传照片 * 返回key * * // * @param path 本地路径 * * @param objectKey 文件名 * @return key */ //格式: photo/sfdsgfsdvsdfdsfs.jpg，上传到远程到photo目录里 private static String getObjectImageKey(String objectKey) { return String.format("photo/%s", objectKey); } /** * 上传头像 * // * @param path * * @param objectKey 文件名 * @return */ //格式: image/sfdsgfsdvsdfdsfs.jpg private static String getObjectPortraitKey(String objectKey) { return String.format("image/%s", objectKey); } //格式: audio/201805/sfdsgfsdvsdfdsfs.mp3 private static String getObjectAudioKey(String path) { String fileMd5 = HashUtil.getMD5String(new File(path)); String dateString = getDateString(); return String.format("audio/%s/%s.mp3", dateString, fileMd5); } } 参考：Android使用阿里云OSS存储文件上传
今天遇到一个RN传值的问题。Native代理网络请求的情况下，如果是PB请求，后台返回的数据是一个byte[]数组。但是RN数据传送只能传String类型的。可以通过fastJson装成Json字符串，转换过程中，会自动把byte[]类型的value转成base64字符串。RN页面端接收到数据之后，需要将数据进行PB解析，PB解析需要一个byte[]类型的数据（JS里是Uint8Array）。所以第一步要将base64字符串转换成byte[]数组。这里给出几个base64与byte数组互转的方法，供大家参考。一、base64字符串转byte[]数组方法一：window.atob() //base64转字节 function _base64ToArrayBuffer(base64) { var binary_string = window.atob(base64);//解码使用base64编码的字符串 var len = binary_string.length; //获取长度 var bytes = new Uint8Array(len); for (var i = 0; i < len; i++) { bytes[i] = binary_string.charCodeAt(i); } console.log(bytes) //打印解析出来的byte return bytes.buffer; } _base64ToArrayBuffer("CgRTMDAxEgRDSDAxHQjMBkMlsp0XwA==")方法二：原生JavaScript代码将base64转换成byte数组再转换成字符串var BASE64B = [62,-1,-1,-1,63,52,53,54,55,56,57,58,59,60,61,-1,-1,-1,-1,-1,-1,-1,0,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,-1,-1,-1,-1,-1,-1,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]; //'A'=65,'a'=97,'0'=48,'+'=43,'/'=47 统一减43 //将base64转换成byte数组再转换成字符串 function decode(params,ascii) { if (params == null) return null; if (typeof params === "string") params = stringToBytes(params,ascii); //该方法只适用于utf-8编码和ascii编码 if (params.length%4 != 0) return null; var length = params.length/4*3;if (params[params.length-2] == 61) length -= 2; else if (params[params.length-1] == 61) length -= 1; var result = new Array(); var index = 0; for (var i=0;i<params.length-4;i+=4) { var bits = (BASE64B[params[i]-43] & 0xff) << 18 | (BASE64B[params[i+1]-43] & 0xff) << 12 | (BASE64B[params[i+2]-43] & 0xff) << 6 | (BASE64B[params[i+3]-43] & 0xff); //通过BASE64B[params[i]-43]将原始数值进行还原,然后再向左移位 result[index++] = ((bits >>> 16) & 0xff); //&0xff表示保留8位数(类似求余),>>表示向右移多少位,低位会被丢弃,高位补0(无符号)或1(有符号),>>>表示向右移多少位,高位补0(无符号) result[index++] = ((bits >>> 8) & 0xff); result[index++] = (bits & 0xff); } if (params[params.length-2] == 61) { var bits = (BASE64B[params[params.length-4]-43] & 0xff) << 6 | (BASE64B[params[params.length-3]-43] & 0xff); result[index] = ((bits >>> 4) & 0xff); } else if (params[params.length-1] == 61) { var bits = (BASE64B[params[params.length-4]-43] & 0xff) << 12 | (BASE64B[params[params.length-3]-43] & 0xff) << 6 | (BASE64B[params[params.length-2]-43] & 0xff); result[index++] = ((bits >>> 10) & 0xff); result[index] = ((bits >>> 2) & 0xff); } else { var bits = (BASE64B[params[params.length-4]-43] & 0xff) << 18 | (BASE64B[params[params.length-3]-43] & 0xff) << 12 | (BASE64B[params[params.length-2]-43] & 0xff) << 6 | (BASE64B[params[params.length-1]-43] & 0xff); result[index++] = ((bits >>> 16) & 0xff); result[index++] = ((bits >>> 8) & 0xff); result[index] = (bits & 0xff); } return bytesToString(result,ascii); }二、byte[]数组转base64字符串方法一：window.btoa() var arry = [-1, -40, -1, -32, 0, 16, 74, 70, 73, 70, 0, 1, 2, 0, 0, 1, 0, 1, 0, 0, -1, -37, 0, 67, 0, 8, 6, 6, 7, 6, 5, 8, 7, 7, 7, 9, 9, 8, 10, 12, 20, 13, 12, 11, 11, 12, 25, 18, 19, 15, 20, 29, 26, 31, 30, 29, 26, 28, 28, 32, 36, 46, 39, 32, 34, -39]; var str12 = arrayBufferToBase64(arry);//转换字符串 console.log(str12); var outputImg = document.createElement('img'); outputImg.src = 'data:image/png;base64,'+str12; // // append it to your page document.body.appendChild(outputImg); console.log(outputImg); function arrayBufferToBase64( buffer ) { var binary = ''; var bytes = new Uint8Array( buffer ); var len = bytes.byteLength; for (var i = 0; i < len; i++) { binary += String.fromCharCode( bytes[ i ] ); } return window.btoa( binary ); } 方法二：原生JavaScript代码将byte数组(或字符串)转换成base64var BASE64C = [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,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,48,49,50,51,52,53,54,55,56,57,43,47]; function encode(params,ascii) { //将byte数组(或字符串)转换成base64 if (params == null) return null; if (typeof params === "string") params = stringToBytes(params,ascii); //该方法只适用于utf-8编码和ascii编码 var result = new Array(); //每3个字节一组,重组为4个字节一组 var index = 0; for (var i=0;i<parseInt(params.length/3)*3;i+=3) { //除3取整再乘3可以取到最后面的3的倍数个 var bits = (params[i] & 0xff) << 16 | (params[i+1] & 0xff) << 8 | (params[i+2] & 0xff); //&0xff表示由byte转int,<<表示向左移多少位,高位会被丢弃,低位会补0 result[index++] = BASE64C[(bits >>> 18) & 0x3f]; //&0x3f表示保留6位数(类似对64求余),>>表示向右移多少位,低位会被丢弃,高位补0(无符号)或1(有符号),>>>表示向右移多少位,高位补0(无符号) result[index++] = BASE64C[(bits >>> 12) & 0x3f]; result[index++] = BASE64C[(bits >>> 6) & 0x3f]; result[index++] = BASE64C[bits & 0x3f]; } if (params.length%3 == 1) { //多余1个加两个=号 var bits = (params[params.length-1] & 0xff) << 4; result[index++] = BASE64C[(bits >>> 6) & 0x3f]; result[index++] = BASE64C[bits & 0x3f]; result[index++] = 61; //stringToBytes('=') result[index] = 61; } else if (params.length%3 == 2) { //多余2个加一个=号 var bits = (params[params.length-2] & 0xff) << 10 | (params[params.length-1] & 0xff) << 2; result[index++] = BASE64C[(bits >>> 12) & 0x3f]; result[index++] = BASE64C[(bits >>> 6) & 0x3f]; result[index++] = BASE64C[bits & 0x3f]; result[index] = 61; } return bytesToString(result); }工具方法上述JS转换代码中用到的string转换工具方法： function stringToBytes(param,ascii) { //该方法只适用于utf-8编码和ascii编码(适用于生成文件),参数为string var bytes = new Array(); if (ascii) { for (var i=0;i<param.length;i++) { bytes.push(param.charCodeAt(i)); } return bytes; } for (var i=0;i<param.length;i++) { var c = param.charCodeAt(i); if (c == 0) { //兼容ascii编码向utf8转码,一般用不到 bytes.push(0xe3); //0xe3=227 bytes.push(0x84); //0x84=132 bytes.push(0x80); //0x80=128 } else if (c < 0x80) { //c < 128,首位为0,剩余7位 bytes.push(c); } else if (c < 0x100) { //c < 256,兼容ascii编码向utf8转码,一般用不到 bytes.push(0xc2); //0xc2=194 bytes.push(c); } else if (c < 0x800) { //c < 2048,首位为110,表示该起始字节有1个后续字节,剩余5位 bytes.push(((c >> 6) & 0x1f) | 0xc0); //0xC0=11000000,&0x1f表示取低5位(高位补0) bytes.push((c & 0x3f) | 0x80); //0x80=10000000,&0x3f表示取低6位(对64求余) } else if (c < 0x10000) { //c < 65536,首位为1110,表示该起始字节有2个后续字节,剩余4位 bytes.push(((c >> 12) & 0x0f) | 0xe0); //0xE0=11100000,&0x0f表示取低4位(对16求余,高位补0) bytes.push(((c >> 6) & 0x3f) | 0x80); bytes.push((c & 0x3f) | 0x80); } else if (c < 0x10ffff) { //c < 2097152,首位为11110,表示该起始字节有3个后续字节,剩余3位 bytes.push(((c >> 18) & 0x07) | 0xf0); //0xF0=11110000,&0x07表示取低3位(高位补0) bytes.push(((c >> 12) & 0x3f) | 0x80); bytes.push(((c >> 6) & 0x3f) | 0x80); bytes.push((c & 0x3f) | 0x80); } else return null; //超过0x10ffff,属于不合法字符 } return bytes; } function bytesToString(params,ascii) { //该方法只适用于utf-8编码和ascii编码,参数为byte数组 var result = ""; if (ascii) { for (var i=0;i<params.length;i++) { result += String.fromCharCode(params[i]); } return result; } for (var i=0;i<params.length;i++) { if (params[i] >= 0xf8) { //超过0xf8=11111000,属于不合法字符 result += String.fromCharCode(params[i]); } else if (params[i] >= 0xf0) { //0xf0=11110000,表示该起始字节有3个后续字节 var bits = (params[i] & 0x07) << 18 | (params[i+1] & 0x3f) << 12 | (params[i+2] & 0x3f) << 6 | (params[i+3] & 0x3f); result += String.fromCharCode(bits); i += 3; } else if (params[i] >= 0xe0) { //0xe0=11100000,表示该起始字节有2个后续字节 var bits = (params[i] & 0x0f) << 12 | (params[i+1] & 0x3f) << 6 | (params[i+2] & 0x3f); result += String.fromCharCode(bits); i += 2; } else if (params[i] >= 0xc0) { //0xc0=11000000,表示该起始字节有1个后续字节 var bits = (params[i] & 0x1f) << 6 | (params[i+1] & 0x3f); result += String.fromCharCode(bits); i++; } else { //[227,132,128],[194,128]的情形已经融入到上面的判断语句中 result += String.fromCharCode(params[i]); } } return result; }参考JavaScript生成Base64码(含String和byte数组互转)base64转byte jsjs中将byte数组转换成base64图片显示
发布出去的APP，签名是必须要的，为什么要签名？签名是为了证明APP的所有权是归公司所有。签名可以是CA认证的证书签名文件，也可以是自签名的文件。下面提供一种签名后的APP，在代码里获取签名信息的方式。该签名信息可以用于app加解密时的唯一密钥，或者在此基础上进行二次加工的结果作为密钥。既保证了密钥的安全性，也保证的密码的强度。获取签名信息 public static String getPublicKeyStrFromPackage() { PublicKey publicKey = getPublicKeyFromPackage(); return publicKey != null ? publicKey.toString() : ""; } public static PublicKey getPublicKeyFromPackage() { String packageName = IGlobalAppContainer.getInstance().getContext().getPackageName(); PublicKey publicKey = null; try { PackageInfo packageInfo = IGlobalAppContainer.getInstance().getContext().getPackageManager().getPackageInfo(packageName, 64); Signature[] signs = packageInfo.signatures; Signature sign = signs[0]; CertificateFactory certFactory = CertificateFactory.getInstance("X.509"); X509Certificate cert = (X509Certificate)certFactory.generateCertificate(new ByteArrayInputStream(sign.toByteArray())); publicKey = cert.getPublicKey(); Log.d("public key = " + publicKey); } catch (CertificateException | NameNotFoundException var7) { var7.printStackTrace(); } return publicKey; } 通过签名信息进行加解密（仅供参考）加密： public static String encryptByPublicKey(String content) throws Exception { String publicKey = getPublicKeyStrFromPackage(); if (!TextUtils.isEmpty(publicKey)) { int start = !publicKey.contains("modulus=") ? 0 : publicKey.indexOf("modulus=") + 8; int end = publicKey.indexOf(",publicExponent="); publicKey = publicKey.substring(start, end); } publicKey = MD5Tools.getMD5(publicKey); return encrypt(content, publicKey); }解密： public static String decryptByPublicKey(String content) { try { String publicKey = getPublicKeyStrFromPackage(); if (!TextUtils.isEmpty(publicKey)) { int start = !publicKey.contains("modulus=") ? 0 : publicKey.indexOf("modulus=") + 8; int end = publicKey.indexOf(",publicExponent="); publicKey = publicKey.substring(start, end); } publicKey = MD5Tools.getMD5(publicKey); return decrypt(content, publicKey); } catch (NoSuchAlgorithmException var4) { Log.w("解密失败！ " + var4.toString()); return content; } }
apply plugin，声明是Android应用程序还是库模块android 闭包，配置项目构建的各种属性，compileSdkVersion用于指定项目的编译SDK版本，buildToolsVersion用于指定项目构建工具的版本。defaultConfig闭包：默认配置，应用程序包名，最小 sdk 版本，目标 sdk 版本，版本号，版本名buildTypes闭包：指定生成安装文件的配置，是否对代码进行混淆signingConfigs 闭包：签名信息配置sourceSets 闭包：源文件路径配置lintOptions 闭包：lint 配置dependencies 闭包，指定当前项目的所有依赖关系，本地依赖，库依赖以及远程依赖repositories闭包，仓库配置 // 声明是Android程序， //com.android.application 表示这是一个应用程序模块,可直接运行 //com.android.library 标识这是一个库模块，是依附别的应用程序运行 apply plugin: 'com.android.application' android { //程序在编译的时候会检查lint，有任何错误提示会停止build，我们可以关闭这个开关 lintOptions { //即使报错也不会停止打包 abortOnError false //打包release版本的时候是否进行检测 checkReleaseBuilds false } //编译sdk的版本，也就是API Level，例如API-19、API-20、API-21等等。 compileSdkVersion 26 //build tools的版本，其中包括了打包工具aapt、dx等等。 //这个工具的目录位于你的sdk目录/build-tools/下 buildToolsVersion '26.0.2' //关闭Android Studio的PNG合法性检查 aaptOptions.cruncherEnabled = false aaptOptions.useNewCruncher = false defaultConfig { //默认配置 applicationId "com.hebbe.espressotest" //应用程序的包名 minSdkVersion 22 //最小sdk版本，如果设备小于这个版本或者大于maxSdkVersion将无法安装这个应用 targetSdkVersion 26 //目标sdk版本，充分测试过的版本（建议版本） versionCode 1 //版本号，第一版是1，之后每更新一次加1 versionName "1.0" //版本名，显示给用户看到的版本号 archivesBaseName = "weshare-$versionName" //指定打包成Jar文件时候的文件名称 ndk { moduleName "hebbewifisafe" //设置库(so)文件名称 ldLibs "log", "z", "m", "jnigraphics", "android" //引入库，比如要用到的__android_log_print abiFilters "armeabi", "x86", "armeabi-v7a" //, "x86" 显示指定支持的ABIs cFlags "-std=c++11 -fexceptions" // C++11 stl "gnustl_static" } //当方法数超过65535(方法的索引使用的是一个short值， //而short最大值是65535)的时候允许打包成多个dex文件，动态加载dex。这里面坑很深啊 multiDexEnabled true //Instrumentation单元测试 testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } //默认的一些文件路径的配置 sourceSets { main { manifest.srcFile 'AndroidManifest.xml'//指定清单文件 res.srcDirs = ['res']//指定res资源目录 assets.srcDirs = ['assets'] //asset资源目录 jni.srcDirs 'src/main/jni' //jni代码目录 jniLibs.srcDir 'src/main/jniLibs' //jni库目录 java.srcDirs = ['src']//指定java源代码目录 resources.srcDirs = ['src']//指定resource目录 aidl.srcDirs = ['src']//指定aidl目录 renderscript.srcDirs = ['src']//指定source目录 } debug.setRoot('build-types/debug')//指定debug模式的路径 release.setRoot('build-types/release')//指定release模式的路径 } //multiDex的一些相关配置，这样配置可以让你的编译速度更快 dexOptions { //让它不要对Lib做preDexing preDexLibraries = false //开启incremental dexing,优化编译效率，这个功能android studio默认是关闭的。 incremental true javaMaxHeapSize "4g" //增加java堆内存大小 } signingConfigs {//签名配置 release {//发布版签名配置 storeFile file("fk.keystore")//密钥文件路径 storePassword "123456"//密钥文件密码 keyAlias "fk"//key别名 keyPassword "123456"//key密码 } debug {//debug版签名配置 storeFile file("fk.keystore") storePassword "123456" keyAlias "fk" keyPassword "123456" } } //指定生成安装文件的配置，常有两个子包:release,debug，注：直接运行的都是debug安装文件 buildTypes { //release版本的配置，即生成正式版安装文件的配置 release { zipAlignEnabled true //是否支持zip shrinkResources true // 移除无用的resource文件 minifyEnabled false //是否对代码进行混淆，true表示混淆 //指定混淆时使用的规则文件； // proguard-android.txt指所有项目通用的混淆规则，proguard-rules.pro当前项目特有的混淆规则 //release的Proguard默认为Module下的proguard-rules.pro文件 proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' debuggable false //是否支持调试 //ndk的一些配置 ndk { // cFlags "-std=c++11 -fexceptions -O3 -D__RELEASE__" // C++11 // platformVersion = "19" moduleName "hebbewifisafe" //设置库(so)文件名称 ldLibs "log", "z", "m", "jnigraphics", "android" //引入库，比如要用到的__android_log_print abiFilters "armeabi", "x86", "armeabi-v7a"//, "x86" cFlags "-std=c++11 -fexceptions" // C++11 stl "gnustl_static" } //采用动态替换字符串的方式生成不同的release.apk applicationVariants.all { variant -> variant.outputs.each { output -> def outputFile = output.outputFile if (outputFile != null && outputFile.name.endsWith('release.apk')) { def timeStamp = new Date().format('yyyyMMddHH'); def fileName = "WeShare-${defaultConfig.versionName}" + "-" + timeStamp + "-lj-" + ".apk"; output.outputFile = file("${outputFile.parent}/${fileName}") } } } jniDebuggable false //关闭jni调试 } debug {//debug版本的配置 minifyEnabled false zipAlignEnabled true shrinkResources true // 移除无用的resource文件 proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' debuggable true // jniDebuggable true ndk { cFlags "-std=c++11 -fexceptions -g -D __DEBUG__" // C++11 } jniDebuggable true } } packagingOptions { exclude 'META-INF/ASL2.0' exclude 'META-INF/LICENSE' exclude 'META-INF/NOTICE' exclude 'META-INF/MANIFEST.MF' } compileOptions { //在这里你可以进行 Java 的版本配置， //以便使用对应版本的一些新特性 } productFlavors { //在这里你可以设置你的产品发布的一些东西， //比如你现在一共软件需要发布到不同渠道， //且不同渠道中的包名不同，那么可以在此进行配置； //甚至可以设置不同的 AndroidManifest.xml 文件。 hebbe { } googlePlay { } solo { } } productFlavors.all { flavor -> flavor.manifestPlaceholders = [UMENG_CHANNEL_VALUE: name] } //所谓ProductFlavors其实就是可定义的产品特性， //配合 manifest merger 使用的时候就可以达成在一次编译 //过程中产生多个具有自己特性配置的版本。 //上面这个配置的作用就是，为每个渠道包产生不同的 UMENG_CHANNEL_VALUE 的值。 } //指定当前项目的所有依赖关系：本地依赖、库依赖、远程依赖 //本地依赖：可以对本地Jar包或目录添加依赖关系 //库依赖：可以对项目中的库模块添加依赖关系 //远程依赖：可以对jcenter库上的开源项目添加依赖 //标准的远程依赖格式是 域名:组织名:版本号 dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) //本地依赖 //远程依赖，com.android.support是域名部分，appcompat-v7是组名称，26.1.0是版本号 implementation 'com.android.support:appcompat-v7:26.1.0' implementation 'com.android.support.constraint:constraint-layout:1.0.2' implementation project(':hello')//库依赖 testImplementation 'junit:junit:4.12' //声明测试用列库 androidTestImplementation 'com.android.support.test:runner:1.0.1' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1' } //声明是要使用谷歌服务框架 apply plugin: 'com.google.gms.google-services' //第三方依赖库的本地缓存路径 task showMeCache << { configurations.compile.each { println it } } //使用maven仓库。android有两个标准的library文件服务器，一个jcenter一个maven。两者毫无关系。 //jcenter有的maven可能没有，反之亦然。 //如果要使用jcenter的话就把mavenCentral()替换成jcenter() repositories { mavenCentral() } 参考《gradle使用教程，一篇就够》《build.gradle详解》
解决Android 国密SM4CBC加解密 导致okhttp报错：No System TLS / BKS not found
APP国密改造已进入尾声，最近一次是埋点库里埋点数据的加解密，需改造成国密。业务代码合入主工程代码时，运行APP挂掉了。提示如下错误信息： 2022-08-01 09:50:01.738 15234-15234/com.maomao.test.activity E/AndroidRuntime: FATAL EXCEPTION: main Process: com.maomao.test.activity:push, PID: 15234 java.lang.RuntimeException: Unable to create application com.maomao.test.activity.tool.MyApp: com.tencent.tinker.loader.TinkerRuntimeException: Tinker Exception:No System TLS at android.app.ActivityThread.handleBindApplication(ActivityThread.java:7409) at android.app.ActivityThread.access$2400(ActivityThread.java:251) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2284) at android.os.Handler.dispatchMessage(Handler.java:110) at android.os.Looper.loop(Looper.java:219) at android.app.ActivityThread.main(ActivityThread.java:8393) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:513) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1055) Caused by: com.tencent.tinker.loader.TinkerRuntimeException: Tinker Exception:No System TLS at com.tencent.tinker.entry.TinkerApplicationInlineFence.onCreateImpl_$noinline$(TinkerApplicationInlineFence.java:104) at com.tencent.tinker.entry.TinkerApplicationInlineFence.onCreate(TinkerApplicationInlineFence.java:110) at com.tencent.tinker.loader.app.TinkerApplication.onCreate(TinkerApplication.java:153) at android.app.Instrumentation.callApplicationOnCreate(Instrumentation.java:1202) at android.app.ActivityThread.handleBindApplication(ActivityThread.java:7393) at android.app.ActivityThread.access$2400(ActivityThread.java:251) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2284) at android.os.Handler.dispatchMessage(Handler.java:110) at android.os.Looper.loop(Looper.java:219) at android.app.ActivityThread.main(ActivityThread.java:8393) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:513) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1055) Caused by: java.lang.AssertionError: No System TLS at okhttp3.internal.Util.assertionError(Util.java:504) at okhttp3.OkHttpClient.newSslSocketFactory(OkHttpClient.java:296) at okhttp3.OkHttpClient.<init>(OkHttpClient.java:258) at okhttp3.OkHttpClient$Builder.build(OkHttpClient.java:1040) at com.maomao.test.activity.net.okhttp.OkHttpFactory.initMTPClient(OkHttpFactory.java:507) at com.maomao.test.activity.net.okhttp.OkHttpFactory.initClient(OkHttpFactory.java:144) at com.maomao.test.activity.net.okhttp.OkHttpFactory.<init>(OkHttpFactory.java:123) at com.maomao.test.activity.net.okhttp.OkHttpFactory.getInstance(OkHttpFactory.java:468) at com.maomao.test.activity.globaldata.GlobalAppContainer.getBaseOKHttpFactory(GlobalAppContainer.java:138) at com.maomao.test.activity.net.NetUtilsImpl.getAsync(NetUtilsImpl.java:33) at com.maomao.test.activity.net.NetUtils.getCms(NetUtils.java:165) at com.maomao.test.activity.net.pb.ProtoBufUtil.queryProtobufList(ProtoBufUtil.java:23) at com.maomao.test.activity.globaldata.InterfaceImp.queryProtobufList(InterfaceImp.java:144) at com.maomao.test.activity.myapp.MyAppLike.onCreate(MyAppLike.java:216) at com.tencent.tinker.entry.TinkerApplicationInlineFence.onCreateImpl_$noinline$(TinkerApplicationInlineFence.java:99) at com.tencent.tinker.entry.TinkerApplicationInlineFence.onCreate(TinkerApplicationInlineFence.java:110) at com.tencent.tinker.loader.app.TinkerApplication.onCreate(TinkerApplication.java:153) at android.app.Instrumentation.callApplicationOnCreate(Instrumentation.java:1202) at android.app.ActivityThread.handleBindApplication(ActivityThread.java:7393) at android.app.ActivityThread.access$2400(ActivityThread.java:251) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2284) at android.os.Handler.dispatchMessage(Handler.java:110) at android.os.Looper.loop(Looper.java:219) at android.app.ActivityThread.main(ActivityThread.java:8393) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:513) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1055) Caused by: java.security.KeyManagementException: java.security.KeyStoreException: BKS not found at com.android.org.conscrypt.SSLParametersImpl.createDefaultX509KeyManager(SSLParametersImpl.java:539) at com.android.org.conscrypt.SSLParametersImpl.getDefaultX509KeyManager(SSLParametersImpl.java:520) at com.android.org.conscrypt.SSLParametersImpl.<init>(SSLParametersImpl.java:128) at com.android.org.conscrypt.OpenSSLContextImpl.engineInit(OpenSSLContextImpl.java:111) at javax.net.ssl.SSLContext.init(SSLContext.java:326) at okhttp3.OkHttpClient.newSslSocketFactory(OkHttpClient.java:293) at okhttp3.OkHttpClient.<init>(OkHttpClient.java:258) at okhttp3.OkHttpClient$Builder.build(OkHttpClient.java:1040) at com.maomao.test.activity.net.okhttp.OkHttpFactory.initMTPClient(OkHttpFactory.java:507) at com.maomao.test.activity.net.okhttp.OkHttpFactory.initClient(OkHttpFactory.java:144) at com.maomao.test.activity.net.okhttp.OkHttpFactory.<init>(OkHttpFactory.java:123) at com.maomao.test.activity.net.okhttp.OkHttpFactory.getInstance(OkHttpFactory.java:468) at com.maomao.test.activity.globaldata.GlobalAppContainer.getBaseOKHttpFactory(GlobalAppContainer.java:138) at com.maomao.test.activity.net.NetUtilsImpl.getAsync(NetUtilsImpl.java:33) at com.maomao.test.activity.net.NetUtils.getCms(NetUtils.java:165) at com.maomao.test.activity.net.pb.ProtoBufUtil.queryProtobufList(ProtoBufUtil.java:23) at com.maomao.test.activity.globaldata.InterfaceImp.queryProtobufList(InterfaceImp.java:144) at com.maomao.test.activity.myapp.MyAppLike.onCreate(MyAppLike.java:216) at com.tencent.tinker.entry.TinkerApplicationInlineFence.onCreateImpl_$noinline$(TinkerApplicationInlineFence.java:99) at com.tencent.tinker.entry.TinkerApplicationInlineFence.onCreate(TinkerApplicationInlineFence.java:110) at com.tencent.tinker.loader.app.TinkerApplication.onCreate(TinkerApplication.java:153) at android.app.Instrumentation.callApplicationOnCreate(Instrumentation.java:1202) at android.app.ActivityThread.handleBindApplication(ActivityThread.java:7393) at android.app.ActivityThread.access$2400(ActivityThread.java:251) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2284) at android.os.Handler.dispatchMessage(Handler.java:110) at android.os.Looper.loop(Looper.java:219) at android.app.ActivityThread.main(ActivityThread.java:8393) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:513) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1055) Caused by: java.security.KeyStoreException: BKS not found at java.security.KeyStore.getInstance(KeyStore.java:890) at com.android.org.conscrypt.KeyManagerFactoryImpl.engineInit(KeyManagerFactoryImpl.java:63) at javax.net.ssl.KeyManagerFactory.init(KeyManagerFactory.java:272) at com.android.org.conscrypt.SSLParametersImpl.createDefaultX509KeyManager(SSLParametersImpl.java:528) at com.android.org.conscrypt.SSLParametersImpl.getDefaultX509KeyManager(SSLParametersImpl.java:520) at com.android.org.conscrypt.SSLParametersImpl.<init>(SSLParametersImpl.java:128) at com.android.org.conscrypt.OpenSSLContextImpl.engineInit(OpenSSLContextImpl.java:111) at javax.net.ssl.SSLContext.init(SSLContext.java:326) at okhttp3.OkHttpClient.newSslSocketFactory(OkHttpClient.java:293) at okhttp3.OkHttpClient.<init>(OkHttpClient.java:258) at okhttp3.OkHttpClient$Builder.build(OkHttpClient.java:1040) at com.maomao.test.activity.net.okhttp.OkHttpFactory.initMTPClient(OkHttpFactory.java:507) at com.maomao.test.activity.net.okhttp.OkHttpFactory.initClient(OkHttpFactory.java:144) at com.maomao.test.activity.net.okhttp.OkHttpFactory.<init>(OkHttpFactory.java:123) at com.maomao.test.activity.net.okhttp.OkHttpFactory.getInstance(OkHttpFactory.java:468) at com.maomao.test.activity.globaldata.GlobalAppContainer.getBaseOKHttpFactory(GlobalAppContainer.java:138) at com.maomao.test.activity.net.NetUtilsImpl.getAsync(NetUtilsImpl.java:33) at com.maomao.test.activity.net.NetUtils.getCms(NetUtils.java:165) at com.maomao.test.activity.net.pb.ProtoBufUtil.queryProtobufList(ProtoBufUtil.java:23) at com.maomao.test.activity.globaldata.InterfaceImp.queryProtobufList(InterfaceImp.java:144) at com.maomao.test.activity.myapp.MyAppLike.onCreate(MyAppLike.java:216) at com.tencent.tinker.entry.TinkerApplicationInlineFence.onCreateImpl_$noinline$(TinkerApplicationInlineFence.java:99) at com.tencent.tinker.entry.TinkerApplicationInlineFence.onCreate(TinkerApplicationInlineFence.java:110) at com.tencent.tinker.loader.app.TinkerApplication.onCreate(TinkerApplication.java:153) at android.app.Instrumentation.callApplicationOnCreate(Instrumentation.java:1202) at android.app.ActivityThread.handleBindApplication(ActivityThread.java:7393) at android.app.ActivityThread.access$2400(ActivityThread.java:251) at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2284) at android.os.Handler.dispatchMessage(Handler.java:110) at android.os.Looper.loop(Looper.java:219) at android.app.ActivityThread.main(ActivityThread.java:8393) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:513) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1055) Caused by: java.security.NoSuchAlgorithmException: BKS KeyStore not available at sun.security.jca.GetInstance.getInstance(GetInstance.java:159) at java.security.Security.getImpl(Security.java:628) 2022-08-01 09:50:01.738 15234-15234/com.maomao.test.activity E/AndroidRuntime: at java.security.KeyStore.getInstance(KeyStore.java:887)看到问题一脸懵，国密改造并没有改网络请求相关的代码。而且本地数据库，密码加密都改造完上线了，最后埋点改造出了问题，不知从何查起；此时我的畏难情绪达到顶点，发愁，无从下手。经过几个日夜的情绪斗争，终于决定正视问题，解决它！问题排查思路历程（一）首先“百度”了一下报错信息“java.lang.AssertionError: No System TLS”和“java.security.KeyStoreException: BKS not found”。全网几乎没有答案，唯一一个匹配的问题，也是没有答案的。最后在这几个博客文章中找到启发：https://www.it1352.com/980912.htmlhttps://www.thinbug.com/q/6866447https://docs.oracle.com/javase/6/docs/technotes/guides/security/crypto/CryptoSpec.html#ProviderInstalling但是这些文章并没有解决我的问题。（二）然后我尝试打印出问题时的providerList里面有哪些内容。发现我添加的代码没有生效。添加provider代码如下： static { Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME); if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { Log.i("sys", "运行环境没有BouncyCastleProvider"); Security.addProvider(new BouncyCastleProvider()); } }我在方法触发之前和触发之后都分别打印了一下所有的provider:Provider[] providers = Security.getProviders(); for (Provider provider : providers) { Log.e("e", "getProviders before provider name:" + provider.getName() + ",version:" + provider.getVersion() + ",className:" + provider.getClass().getName()); } //此处调用static代码块 providers = Security.getProviders(); for (Provider provider : providers) { Log.e("e", "getProviders after provider:" + provider.getName()); } 打印的日志如下： getProviders before provider name:AndroidNSSP,version:1.0,className:android.security.net.config.NetworkSecurityConfigProvider getProviders before provider name:AndroidOpenSSL,version:1.0,className:com.android.org.conscrypt.OpenSSLProvider getProviders before provider name:CertPathProvider,version:1.0,className:sun.security.provider.CertPathProvider getProviders before provider name:AndroidKeyStoreBCWorkaround,version:1.0,className:android.security.keystore.AndroidKeyStoreBCWorkaroundProvider getProviders before provider name:BC,version:1.61,className:com.android.org.bouncycastle.jce.provider.BouncyCastleProvider getProviders before provider name:HarmonyJSSE,version:1.0,className:com.android.org.conscrypt.JSSEProvider getProviders before provider name:AndroidKeyStore,version:1.0,className:android.security.keystore.AndroidKeyStoreProvider getProviders after provider:AndroidNSSP getProviders after provider:AndroidOpenSSL getProviders after provider:CertPathProvider getProviders after provider:AndroidKeyStoreBCWorkaround getProviders after provider:HarmonyJSSE getProviders after provider:AndroidKeyStore有两个发现。一个是android自带了BouncyCastleProvider，版本是1.61第二个是我执行的Security.addProvider(new BouncyCastleProvider());代码没有生效。网上找到一篇关于setProvider不生效的帖子：https://www.cnblogs.com/zhangyy3/p/13672261.html于是我怀疑是我BC库的jar包没有签名导致给Java设置Security的值无法验证，导致插入失败。jar包文件签名步骤：Java/Android ：Jar包签名原本是想签名一下jar包再继续验证，但是签名路程坎坷，不方便拿到签名文件，所以这条路走不通。然后又发现另一个疑点：我的BC库版本比Android自带的BC库版本低。有可能是低版本无法覆盖高版本的BC库，导致插入失败。解决版本问题需要把BC库源码下载下来，改一下BouncyCastleProvider.java的name，起一个不叫“BC”的名字。就不会与系统BC库冲突。（这个方案是领导想出来的，他觉得非常有把握。我认为风险很高，手动编译源码，不知道会有多少坑等着我……）这个方案或许能解决问题，但是如果Java在Security.addProvider时需要验证签名，改了也是白搭。（三）后面，转念一想：既然我可以改BouncyCastleProvider.java里的源码，为啥不自己自定义一个Provider，然后add进去试试呢？如果自定义的Provider能插入进去，说明Java在Security.addProvider时不验证签名，可以尝试改源码的方案。于是我将BouncyCastleProvider.java源码拷贝了一份，将里面的public static final String PROVIDER_NAME = "BC";改成了public static final String PROVIDER_NAME = "BCTEMP";然后测试代码改成了Provider[] providers = Security.getProviders(); for (Provider provider : providers) { Log.e("e", "getProviders before provider name:" + provider.getName() + ",version:" + provider.getVersion() + ",className:" + provider.getClass().getName()); } Security.addProvider(new BouncyCastleProvider()); providers = Security.getProviders(); for (Provider provider : providers) { Log.e("e", "getProviders after provider:" + provider.getName()); } 打印日志如下：getProviders before provider name:AndroidNSSP,version:1.0,className:android.security.net.config.NetworkSecurityConfigProvider getProviders before provider name:AndroidOpenSSL,version:1.0,className:com.android.org.conscrypt.OpenSSLProvider getProviders before provider name:CertPathProvider,version:1.0,className:sun.security.provider.CertPathProvider getProviders before provider name:AndroidKeyStoreBCWorkaround,version:1.0,className:android.security.keystore.AndroidKeyStoreBCWorkaroundProvider getProviders before provider name:BC,version:1.61,className:com.android.org.bouncycastle.jce.provider.BouncyCastleProvider getProviders before provider name:HarmonyJSSE,version:1.0,className:com.android.org.conscrypt.JSSEProvider getProviders before provider name:AndroidKeyStore,version:1.0,className:android.security.keystore.AndroidKeyStoreProvider getProviders after provider:AndroidNSSP getProviders after provider:AndroidOpenSSL getProviders after provider:CertPathProvider getProviders after provider:AndroidKeyStoreBCWorkaround getProviders after provider:HarmonyJSSE getProviders after provider:AndroidKeyStore getProviders after provider:BCTEMP 验证结果：名叫“BCTEMP”的provider添加成功了。这个结果说明：Security.addProvider不校验签名，任何继承Provider的类都可以被添加进去；这就更加无法解释为啥我之前的Security.addProvider没有添加成功了。第一个签名的疑问已经打消，因为自定义代码没有签名也能生效；第二个版本低的问题，因为我是先删除后添加，理论上不存在版本覆盖的问题。（四）于是我又多打印了一些日志。在下面这段代码 static { Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME); if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) { Log.i("sys", "运行环境没有BouncyCastleProvider"); Security.addProvider(new BouncyCastleProvider()); } }的前前后后也加了日志： static { Log.e("e", "static current Thread name:" + Thread.currentThread().getName() + ",Thread id:" + Thread.currentThread().getName()); Provider[] providers = Security.getProviders(); for (Provider provider : providers) { Log.e("e", "static getProviders before provider name:" + provider.getName() + ",version:" + provider.getVersion() + ",className:" + provider.getClass().getName()); } Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME); Log.e("e", "static getProviders remove provider result:" + Security.getProvider(BouncyCastleProvider.PROVIDER_NAME)); providers = Security.getProviders(); for (Provider provider : providers) { Log.e("e", "static getProviders remove provider name:" + provider.getName() + ",version:" + provider.getVersion() + ",className:" + provider.getClass().getName()); } int result = Security.addProvider(new BouncyCastleProvider()); Log.e("e", "static getProviders add provider result:" + result); providers = Security.getProviders(); for (Provider provider : providers) { Log.e("e", "static getProviders add provider name:" + provider.getName() + ",version:" + provider.getVersion() + ",className:" + provider.getClass().getName()); } }打印结果： getProviders before provider name:AndroidNSSP,version:1.0,className:android.security.net.config.NetworkSecurityConfigProvider getProviders before provider name:AndroidOpenSSL,version:1.0,className:com.android.org.conscrypt.OpenSSLProvider getProviders before provider name:CertPathProvider,version:1.0,className:sun.security.provider.CertPathProvider getProviders before provider name:AndroidKeyStoreBCWorkaround,version:1.0,className:android.security.keystore.AndroidKeyStoreBCWorkaroundProvider getProviders before provider name:BC,version:1.61,className:com.android.org.bouncycastle.jce.provider.BouncyCastleProvider getProviders before provider name:HarmonyJSSE,version:1.0,className:com.android.org.conscrypt.JSSEProvider getProviders before provider name:AndroidKeyStore,version:1.0,className:android.security.keystore.AndroidKeyStoreProvider static getProviders before provider name:AndroidNSSP,version:1.0,className:android.security.net.config.NetworkSecurityConfigProvider static getProviders before provider name:AndroidOpenSSL,version:1.0,className:com.android.org.conscrypt.OpenSSLProvider static getProviders before provider name:CertPathProvider,version:1.0,className:sun.security.provider.CertPathProvider static getProviders before provider name:AndroidKeyStoreBCWorkaround,version:1.0,className:android.security.keystore.AndroidKeyStoreBCWorkaroundProvider static getProviders before provider name:BC,version:1.61,className:com.android.org.bouncycastle.jce.provider.BouncyCastleProvider static getProviders before provider name:HarmonyJSSE,version:1.0,className:com.android.org.conscrypt.JSSEProvider static getProviders before provider name:AndroidKeyStore,version:1.0,className:android.security.keystore.AndroidKeyStoreProvider getProviders after provider name:AndroidNSSP,version:1.0,className:android.security.net.config.NetworkSecurityConfigProvider getProviders after provider name:AndroidOpenSSL,version:1.0,className:com.android.org.conscrypt.OpenSSLProvider getProviders after provider name:CertPathProvider,version:1.0,className:sun.security.provider.CertPathProvider getProviders after provider name:AndroidKeyStoreBCWorkaround,version:1.0,className:android.security.keystore.AndroidKeyStoreBCWorkaroundProvider getProviders after provider name:BC,version:1.61,className:com.android.org.bouncycastle.jce.provider.BouncyCastleProvider getProviders after provider name:HarmonyJSSE,version:1.0,className:com.android.org.conscrypt.JSSEProvider getProviders after provider name:AndroidKeyStore,version:1.0,className:android.security.keystore.AndroidKeyStoreProvider static getProviders after provider name:AndroidOpenSSL,version:1.0,className:com.android.org.conscrypt.OpenSSLProvider static getProviders after provider name:CertPathProvider,version:1.0,className:sun.security.provider.CertPathProvider static getProviders after provider name:AndroidKeyStoreBCWorkaround,version:1.0,className:android.security.keystore.AndroidKeyStoreBCWorkaroundProvider static getProviders after provider name:BC,version:1.61,className:com.android.org.bouncycastle.jce.provider.BouncyCastleProvider static getProviders after provider name:HarmonyJSSE,version:1.0,className:com.android.org.conscrypt.JSSEProvider static getProviders after provider name:AndroidKeyStore,version:1.0,className:android.security.keystore.AndroidKeyStoreProvider static getProviders after provider name:BC,version:1.58,className:com.org.bouncycastle.jce.provider.BouncyCastleProvider验证发现：provider name:BC,version:1.58已经被添加进去了。getProviders after provider的日志在static getProviders after provider之前打印。这个结果说明：static{}代码块执行Security.addProvider,是在在调用者调用完static{}代码块代码之后。static{}代码块执行和调用者代码执行不在同一个线程导致了1的结果。随后我添打印了一下各自当前线程，得到了证实，确实不在同一线程。问题根本原因是：static{}代码块是在子线程执行的（因为埋点调用国密加解密是在子线程执行），而调用方是主线程（埋点调用方和okhttp初始化同在主线程）。在执行Security.removeProvider之后，又进入了主线程中执行okhttp初始化。而okhttp初始化需要用到名叫“BC”的Provider，此时“BC”的Provider正好已经被删除了，新的Security.addProvider还没添加进来，导致okhttp初始化时报错：No System TLS / BKS not found解决方案找到问题原因就好办了。现在问题演变成了多线程同步问题，如何保证在okhttp初始化之前，“BC”Provider已经初始化好了。所以最后解决方案是：把static{}代码块执行的时机，放在埋点库调用国密加解密、和okhttp初始化之前，并且在主线程执行。这样，可以保证埋点库调用国密加解密开子线程之前，static{}代码块已经执行过了。利用线程之间数据共享和static{}代码块只会执行一次的原理，后面执行埋点库开子线程调用国密加解密时，static{}代码块已经执行过，且无需再执行static{}代码块就能进行后面的操作。小结这是一次超乎想象的难题，问题起因毫无头绪，排查非常困难，而且影响面极大，应用初始化的时候就crash了。我有时候会逃避问题，见到这么棘手的问题，要是百度不能解决，我就很想放弃。但是这是个大问题，埋点库必须要上线，国密也必须要上线，APP坚决不能崩溃！所以只能硬着头皮上了。回过头来发现：在毫无头绪的时候，多打印日志观察现象，是一个好的解决思路。逃避不能解决任何问题。最后还是要回到问题本身，正视问题，观察问题原因，不断怀疑提问，然后顺着自己的提问判断假设是否成立，总结出问题的真正原因所在。这次的经验对我非常重要。我才发现“畏难心理”是一个非常不利于帮助解决问题的情绪。再大的问题，只要你慢慢跟他耗，总会有解决的一天，但是如果什么都不做，那就会毫无进展。勇敢毛毛，不怕困难。冲呀～