众所周知,腾讯的X5内核除了支持webview浏览以外,还支持文件浏览,如果以前就用过的,就知道是这么调用的:
QbSdk.openFileReader(ct, file.getAbsolutePath(), null, null);
然而最近查看官方文档,不管是腾讯浏览服务还是腾讯接入文档都没有相关的资料,再仔细查,发现在常见问题列表当中有这么一个:
点击进去,咦咦咦,怎么内容没有了
可能因为商业调整原因吧。之前我看过里面的内容,大致意思是原来的TBS文件浏览能力不稳定,为了更好地提供定制需求,TBS团队专门剥离了原来的文件浏览能力并进行优化,生成一款新产品,然后我们咨询了一下,价格不菲,于是我们产品经理让我再想想办法。。。
一般地,如果没有特殊需求,要使用文件浏览服务只需要调用本文最开始的TBS接口就行了,但是,我们的产品要求不允许调用第三方APP打开,更不允许界面右上角能点击进入分享弹窗。我找了一圈发现市场上并没有其他专门做文件浏览的产品,于是又回到了TBS。它现在还是免费,但是依据官方文档操作确实有不稳定的问题,内核下发常常很慢,这是不能容忍的。我就想,能不能做成静态加载呢?
我的思路是,把TBS内核从APP的/data/data/包名/路径下复制出来,删掉多余的东西,放在assets里面,使用时再解压放回去。
开干!
经过测试,发现32位手机的内核放在app_tbs文件夹里,64位的放在app_tbs_64文件夹里,删除掉多余的so库(位于/app_tbs/core_share或/app_tbs_64/core_share文件夹里):
这是最占空间的,当然如果你的应用需要X5 webview浏览功能这个就千万不能删;还有其他看着没有绝对必要的库我也一并删除了。还有就是,并不是在这部手机下载的64位内核就能用于另一部手机,经过我反复测试,终于下载到了一版能够通用的(手头上5、6部手机都能正常运行)
将整个文件夹压缩,放入assets:
接下来就是代码实现:
class FileScanActivity : BaseActivity(), CoroutineScope by MainScope() {
private var job: Job = Job()
private var path = ""
companion object {
const val APP_TBS_ZIP = "tbs.zip"
const val APP_TBS_64_ZIP = "tbs_64.zip"
const val APP_TBS = "tbs"
const val APP_TBS_64 = "tbs_64"
@JvmStatic
fun startMe(context: Context, path: String) {
val intent = Intent(context, FileScanActivity::class.java)
intent.putExtra("path", path)
context.startActivity(intent)
}
}
override fun initContentView() {
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE or WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN) // 设置默认键盘不弹出
setContentView(R.layout.activity_file_scan)
path = intent.getStringExtra("path")
val name = File(path).name
titleBar.setTitle(name)
}
override fun init() {
var support64 = false
val cpus = Build.SUPPORTED_64_BIT_ABIS
if (cpus.isNotEmpty()) {
//64位架构
support64 = true
}
val tbsFile = getDir(if (support64) APP_TBS_64 else APP_TBS, Context.MODE_PRIVATE)
if (tbsFile.exists() && tbsFile.listFiles().isNotEmpty()) {
for (file in tbsFile.listFiles()) {
if (file.isDirectory && file.name == "core_share" && file.listFiles().isNotEmpty()) {
loadFragment()
return
}
}
}
job = launch {
ProgressDialog.show(context)
val res = withContext(Dispatchers.IO) {
try {
// 将文件从assets目录中复制出来
if (support64) {
val is2 = assets.open(APP_TBS_64_ZIP)
val output2 = openFileOutput(APP_TBS_64_ZIP, Context.MODE_PRIVATE)
output2.use {
is2.copyTo(output2)
}
//解压
FilePathUtils.unZipResourcePackage(File(filesDir.absolutePath + File.separator + APP_TBS_64_ZIP), getDir("", Context.MODE_PRIVATE).absolutePath)
} else {
val inputStream = assets.open(APP_TBS_ZIP)
val output = openFileOutput(APP_TBS_ZIP, Context.MODE_PRIVATE)
output.use {
inputStream.copyTo(output)
}
//解压
FilePathUtils.unZipResourcePackage(File(filesDir.absolutePath + File.separator + APP_TBS_ZIP), getDir("", Context.MODE_PRIVATE).absolutePath)
}
true
} catch (e: IOException) {
e.printStackTrace()
false
}
}
ProgressDialog.closed()
if (res) {
loadFragment()
}
Log.d("FileScanActivity", "copy res: $res")
}
}
private fun loadFragment() {
val fragment = TbsReaderFragment.newFragment(path)
addFragment(fragment, R.id.layout)
}
override fun onDestroy() {
super.onDestroy()
job.cancel()
}
}
class TbsReaderFragment : Fragment() {
var mReaderView: TbsReaderView? = null
var mReaderOpened = false
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val context = context
mReaderView = TbsReaderView(context) { integer, o, o1 -> Log.d("TbsReaderFragment", "1: $integer, 2: $o, 3: $o1") }
return mReaderView
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
arguments?.apply {
val path = getString(INTENT_PATH)
if (!TextUtils.isEmpty(path)) {
open(path)
}
}
}
private fun open(path: String) {
lifecycleScope.launch {
val format = parseFormat(path)
val preOpen = mReaderView!!.preOpen(format, false) // 该状态标志x5文件能力是否成功初始化并支持打开文件的格式
if (preOpen) { // 使用x5内核打开office文件
val b = SharedPrefsHelper.get(PubConstant.TBS_FILE_LOAD, false)
if (b) {
delay(20)
} else {
delay(1000)
SharedPrefsHelper.put(PubConstant.TBS_FILE_LOAD, true)
}
val bundle = Bundle()
bundle.putString("filePath", path)
bundle.putString("tempPath", FilePathUtils.getInstance().downFilePath)
mReaderView?.openFile(bundle)
} else {
toast(R.string.file_no_support)
}
}
}
/**
* 解析文件格式
* @param fileName
* @return
*/
private fun parseFormat(fileName: String): String {
return fileName.substring(fileName.lastIndexOf(".") + 1)
}
override fun onDestroy() {
super.onDestroy()
mReaderView?.onStop()
}
companion object {
private const val TAG = "TbsReaderFragment"
private const val INTENT_PATH = "INTENT_PATH"
fun newFragment(path: String): TbsReaderFragment {
val fragment = TbsReaderFragment()
val argument = Bundle()
argument.putString(INTENT_PATH, path)
fragment.arguments = argument
return fragment
}
}
}
心细的可能发现到了2个问题,
1、为啥要delay,因为内核首次初始化需要比较长的时间,再次使用就比较快了
2、为啥mReaderView?.onStop()要写在onDestroy()方法里面,因为如果按照字面意思写在onStop()里面,你会发现APP退回后台再返回前台,界面的内容被销毁了。。。
然后还有一个需要注意的点,使用TbsReaderView会发生内存泄漏,可能内部有个线程绑定了UI,好在使用多次也只会发生一次,就当作没看见了。
最后,可以把TBS的内核初始化代码注释掉了,再也不用傻乎乎的等着初始化回调是true还是false了:
// QbSdk.setDownloadWithoutWifi(true);
// QbSdk.PreInitCallback cb = new QbSdk.PreInitCallback() {
//
// @Override
// public void onViewInitFinished(boolean arg0) {
// //x5內核初始化完成的回调,为true表示x5内核加载成功,否则表示x5内核加载失败,会自动切换到系统内核。
// Log.d("QbSdk", " onViewInitFinished is " + arg0);
// }
//
// @Override
// public void onCoreInitFinished() {
// Log.d("QbSdk", "onCoreInitFinished");
// }
// };
// //x5内核初始化接口
// QbSdk.initX5Environment(getApplicationContext(), cb);
// // 接入文档地址:https://x5.tencent.com/docs/access.html
// // 在调用TBS初始化、创建WebView之前进行如下配置
// HashMap<String, Object> map = new HashMap<>();
// map.put(TbsCoreSettings.TBS_SETTINGS_USE_SPEEDY_CLASSLOADER, true);
// map.put(TbsCoreSettings.TBS_SETTINGS_USE_DEXLOADER_SERVICE, true);
// QbSdk.initTbsSettings(map);
PS:写DEMO的时候发现,从正式项目拷贝过来的内核不能用,怀疑是某些配置文件引起的,终于让我发现是这个文件的问题:
打开是这样的:
所以如果是直接拷贝的其他项目的内核,需要对该文件做处理(注意:直接在AS上修改该文件会失败)
private fun changeConfig() {
val tbsRootPath = getDir(APP_TBS_64, Context.MODE_PRIVATE).absolutePath
//tbsRootPath: /data/user/0/包名/app_tbs_64
val content = "core_packagename=${packageName}\n" +
"app_version=1\n" +
"core_disabled=false\n" +
"core_version=46011\n" +
"core_path=${tbsRootPath + File.separator}core_share"
val file = File("${tbsRootPath + File.separator}share" + File.separator + "core_info")
val fileWriter = FileWriter(file)
fileWriter.use {
it.write(content)
}
}
PPS:刚又发现,上周32位的手机使用这个方案还可以正常运行,突然就失灵了,凌乱了。。。目前采用的解决办法是判断如果是32位的,还是去下载内核
String[] cpus = Build.SUPPORTED_64_BIT_ABIS;
if (cpus == null || cpus.length <= 0) {
//只有32位架构
QbSdk.setDownloadWithoutWifi(true);
QbSdk.setTbsListener(new TbsListener() {
@Override
public void onDownloadFinish(int progress) {
Log.d("QbSdk", "onDownloadFinish -->下载X5内核完成进度:" + progress);
}
@Override
public void onInstallFinish(int progress) {
Log.d("QbSdk", "onInstallFinish -->安装X5内核进度:" + progress);
}
@Override
public void onDownloadProgress(int progress) {
Log.d("QbSdk", "onDownloadProgress -->下载X5内核进度:" + progress);
}
});
QbSdk.PreInitCallback cb = new QbSdk.PreInitCallback() {
@Override
public void onViewInitFinished(boolean arg0) {
//x5內核初始化完成的回调,为true表示x5内核加载成功,否则表示x5内核加载失败,会自动切换到系统内核。
Log.d("QbSdk", " onViewInitFinished is " + arg0);
if (!arg0) {
TbsDownloader.startDownload(AppContext.this);
}
}
@Override
public void onCoreInitFinished() {
Log.d("QbSdk", "onCoreInitFinished");
}
};
//x5内核初始化接口
QbSdk.initX5Environment(getApplicationContext(), cb);
// 接入文档地址:https://x5.tencent.com/docs/access.html
// 在调用TBS初始化、创建WebView之前进行如下配置
HashMap<String, Object> map = new HashMap<>();
map.put(TbsCoreSettings.TBS_SETTINGS_USE_SPEEDY_CLASSLOADER, true);
map.put(TbsCoreSettings.TBS_SETTINGS_USE_DEXLOADER_SERVICE, true);
QbSdk.initTbsSettings(map);
}
还有一个问题:没加载插件之前,断掉手机网络,会提示加载插件失败。。。意思是还没有完全摆脱掉TBS的网络影响,排查了一番,暂时没有定位到原因。这有可能是个致命问题,将来一旦收费,前面的这些操作都是无用功。费了半天劲,只换来64位手机免下载内核。。。
附上github demo地址:(待完成)
疑虑:是否合法?
但是目前TBS肯定是在做商业化准备,有需求的小伙伴建议尽早做处理
PS:腾讯文档SDK已经商业化了,以上方案作废