动态资源管理系统是货拉拉目前使用的用于管理离线so、动画资源、字体文件的组件,对于减小包体积起着重要作用。具体使用方法参考开源代码中介绍。
https://github.com/HuolalaTech/hll-dynamic-res-plugin
资源加载:将动态资源通过下载,校验,解压等方式,映射到本地文件的过程。
该过程对所有资源通用,sdk使用方无需修改资源加载方式。
资源应用:动态资源对应的本地文件应用到具体业务中。例如动态字体资源的应用,就是为TextView设置一个新的字体。
该过程每个资源不同,sdk使用方无需修改内置资源的应用方式,对于自定义资源,需要使用方自行决定应用方式。
资源打包:包括生成一个待上传的资源文件,以及生成资源的Java描述(DynamicPkgInfo类)。so资源还包含了一些方法的hook操作。
该过程对所有资源都适用,统一使用可配置的dyanmic_res_resplugin插件完成。sdk使用方无需修改资源打包方式,但是可通过配置dyanmic_plugin.gradle文件,配置打包过程。
如何确定资源已经下载过了,避免重复下载?
Java代码中,使用DynamicPkgInfo类来描述资源,该类中包含了资源的版本号。我们比较该类和本地数据库中的资源版本号,如果不同,才会下载资源。
下载资源是否提供多线程下载,断点续传等功能?
本sdk只提供了下载接口,未提供实际下载功能,因此如需这些功能,需要调用者自己实现。
如何校验资源,防止被篡改?
DynamicPkgInfo类中包含了资源校验信息,我们利用该类,对下载好的文件进行md5码,文件长度,文件名称的校验。
如何判断资源是否压缩包,以及如何解压缩?
目前简单的采用后缀名是否为.zip判断,使用使用Java内置java.util.zip包下工具解压。
如何校验解压后的资源子文件,防止被篡改?
DynamicPkgInfo同样包含了zip包中所有子文件的校验信息,我们利用它,来校验所有解压后的文件。
我们使用dynamic_plugin gradle插件来完成所有资源的打包。
字体资源打包
帧动画资源打包
so资源打包
自定义资源打包
运行产物
由于整个系统功能较复杂,我们将其分为3个module。
该库包括了动态资源加载和应用全过程,我们分为5层实现
加载普通资源的主流程如下,首先判断资源包指定版本号和本地数据库版本号是否相同,如果想同,进入本地资源校验流程,否则进入下载流程。
资源实体类中指定的文件名称,文件长度,文件md5码和本地文件相同时,我们认为该文件校验成功了
动态资源加载过程中,可能因为各种原因,导致加载未能得到成功或者失败的结果,而在中间状态被中断,如应用进程被杀死,手机关机等等。为了避免加载意外中断的情况下,完全从头开始进行加载,我们设计了一个动态资源加载的恢复流程,如果异常中断,我们下次加载资源时,可以恢复到当前状态,继续进行加载。
前面我们总结了动态资源的加载流程,资源加载完成后,我们还需要将该资源进行应用,而这里我们要说的就是将动态资源应用到对应View上的流程。
可与第4章,整体架构分层图对照着看
DynamicResManager类负责和外部交互,提供了初始化(init),加载资源(load),isResReady(判断资源是否就绪),clearFailState(清除错误状态等方法)等方法。
Config类,则可以向管理器提供线程池,下载器接口,本地资源信息接口,本地资源状态接口等配置信息。
AbsResInfo抽象类,代表动态资源。
DynamicPkgInfo类,AbsResInfo的子类,提供给外部使用,代表了一个动态资源实体。
DynamicPkgInfo.FileInfo,AbsResInfo的子类,资源实体内部类,代表了资源中的一个子文件。
DynamicPkgInfo.FolderInfo,AbsResInfo的子类,资源实体内部类,代表了资源中的一个子文件夹。
ILoadResListener接口,提供了加载资源时的回调功能,会回调加载成功,失败,状态变化,下载中进度
AbsResApply抽象类,实现了动态资源在ui元素上的应用。
TypefaceResApply类,AbsResApply的子类,代表了字体资源的应用。
FrameAnimApply类,AbsResApply的子类,代表了帧动画资源的应用。
AbsSoLoad抽象类,实现了so动态资源的应用。
RelinkerSoLoad类,AbsSoLoad的子类,使用Relinker第三方库最终load so库。
SystemSoLoad类,AbsSoLoad的子类,使用系统System.loadLibrary方法最终load so库
我们使用状态模式来控制整个动态资源的加载流程。
IState,状态接口,代表了加载流程中的一个状态。
InitState类,初始化状态。
CheckVersionState类,检查资源实体类版本号与数据库版本号是否相同状态。
DownloadState类,下载资源状态。
VerrifyFileState,校验下载资源状态。
UnZipState,解压缩下载资源状态。
VerifyZipState,校验解压后的所有文件状态。
IStateMechine,状态管理机接口,负责管理前面所有的IState对象。
DefaultStateMachine类,状态管理机的默认实现。
ResCtx类,状态管理机运行过程中的全局context对象,存储了路径信息,加载成功信息,加载失败异常等全局信息。
这2层的类,较为杂乱,限于篇幅,我们就不一一列举了。
在打包so资源的过程中,我们遇到了如下问题。
so资源打包解决方案
看到移除 so文件可能有些同学会问,这不是只要在as中删除libs目录就搞定了么?这样会有几个问题
出于以上考虑,我们认为,在编译时期,自动删除并收集so文件是最优解,那么在编译时期进行以上操作呢?我们注意到as在进行build时,会有大量的系统提供的task在运行,那么这些系统task是否就完成了编译并收集各个地方的so文件,并把他们打包进apk的任务呢?
看一眼这幅超级复杂的apk构建流程图,嗯,可以看到,系统确实会在apkBuilder构建前,将本地的c/c++文件编译成so库,并将第三方的so库一起打包到apk中,我们需要寻找的就是收集所有so库的系统Task
通过查找资料,我们发现,确实有2个系统task会用来处理合并so库并且删除debug符号(注意,task名称可能与此处不完全相同)。
Task名称实现类作用结果保存目录mergeDebugNativeLibsMergeNativeLibsTask合并所有依赖的 native 库intermediates/merged_native_libsstripDebugDebugSymbolsStripDebugSymbolsTask从 Native 库中移除 Debug 符号intermediates/stripped_native_libs- 一般来说,应该在stripSymbols结束后去剔除 stripped_native_libs 目录下的文件。
由于我们有多个gradle task需要执行,因此我们创建了一个名为dynamic_res_plugin的android plugin工程,内部包含了多个gradle task。关于as中新建插件的方法,请自行搜索其他博客,本文因为篇幅问题,不进行讲解。
在我们的dynamic_res_plugin插件内部,我们新建一个名为DeleteAndCopySo的gradle task并将它插入到系统的merge和strip之间,利用该Task完成删除merged_native_libs目录下对应so文件,并将其拷贝到我们指定的新目录下。这样apk打包时,就不会包含动态化的so文件了
//获取系统的mergeTask
Task mergeNativeTask = TaskUtil.getMergeNativeTask(project);
//获取系统的skipTask
Task stripTask = TaskUtil.getStripSymbol(project);
//创建我们的DeleteAndCopySo task
Task deleteTask = project.getTasks().create(PluginConst.Task.DELETE_SO);
deleteTask.doLast(new Action<Task>() {
@Override
public void execute(Task task) {
deleteAndCopySo(project, param);
}
});
//将我们的Task插入到merge和strip之间
stripTask.dependsOn(deleteTask);
deleteTask.dependsOn(mergeNativeTask);
上一步中,我们已经将so文件从系统apk构建流程中删除,并且拷贝到了指定目录下。那么现在我们应该做什么呢?
对于上述这些步骤,在我们的货拉拉动态管理系统初始版本中,我们采用了自己打zip包,自己写java代码来生成资源信息的方式。
但是在后来的使用过程中,我们发现,手动进行这些步骤,很繁琐且容易出错,我们需要有一种自动化的方式进行上述过程。
我们在dynamic_res_plugin插件内部,再新增一个ZipSoTask来进行压缩so文件夹,以及生成资源信息常量的操作。该task在DeleteAndCopySo之后,stripe系统task之前执行。
//执行将so文件夹压缩成.zip操作
List<DynamicUtil.ZipInfo> zips = DynamicUtil.zipFolder(new File(param.getmInputSo()),outDir);
//根据so文件和zip压缩包信息,生成md5,length等校验信息并存储
DynamicUtil.createPkgDatas(mPkgList,zips,PluginConst.Type.SO);
//根据资源信息类生产java文件
param.getmFileCreate().createFile(mPkgList,param);
//创建DynamicResConst类,用来存储资源实体常量
TypeSpec.Builder typeBuilder = TypeSpec.classBuilder( "DynamicResConst" )
.addModifiers(Modifier.PUBLIC, Modifier.FINAL);
//遍历资源列表,生成对应实体类DynamicPkgInfo
for (DynamicPkgInfo pkg : pkgs) {
FieldSpec fsc = createField(pkg);
typeBuilder.addField(fsc);
}
//插件java文件,并写入
JavaFile javaFile = JavaFile.builder(param.getmCreateJavaPkgName(), typeBuilder.build()).build();
try {
javaFile.writeTo(new File(param.getmOutputPath()));
} catch (Exception e) {
}
至于最后一步,将so压缩包上传到服务器,我们在配置文件中提供了一个上传方法,不过默认实现为空,用户可以手动上传也可以修改默认方法实现自动上传。自动生成的资源文件中,版本号需要手动修改控制,下载地址手动上传的话,也需要手动修改。
很多三方sdk都要求在应用启动时,进行初始化,一个使用so库的类的典型类代码如下:
public class ThirdLib{
//静态方法加载so库
static{
System.loadLibrary("third");
}
//native方法示例
public native void testNative();
//java方法示例
public void test();
//......其他内容省略
}
具体执行替换的代码如下,在Asm框架中的MethodVisitor类中,重写visitMethodInsn方法,判断该方法的拥有者,名称和参数列表和System.loadLibrary对应,则我们将他替换为我们的SoLoadUtil.loadLibrary方法
@Override
public void visitMethodInsn(int opcode, String owner, String name, String descriptor, boolean isInterface) {
if(TextUtil.equals(owner, PluginConst.SYSTEM_CLASS) &&
TextUtil.equals(name, PluginConst.LOAD_LIBRARY_METHOD) &&
TextUtil.equals(descriptor, PluginConst.LOAD_LIBRARY_DESC)){
owner = "com/xxx/xxx/dynamicres/util/SoLoadUtil" ;
mv.visitMethodInsn(Opcodes.INVOKESTATIC, owner, name, descriptor, false);
return;
}
mv.visitMethodInsn(opcode, owner, name, descriptor, isInterface);
}
替换后的方法主要逻辑为,使用第三方库Relinker替代System.loadLibrary方法进行so文件加载,并且catch住加载异常,来防止应用直接奔溃,并且在加载so库异常时,将该库的名称保存下来,在我们的so包被正常下发加载后,再次调用本方法,将so库load到系统中。
protected void realSoLoad(Context c, String libName) {
try {
ReLinker. recursively ().loadLibrary(c, libName);
removeFormWaitList(libName);
} catch (Throwable t) {
addToWaitList(libName);
}
}
我们只需要在工程的主Application中,直接调用loadSo方法,对so动态资源进行加载。加载完成后,so库就能正常使用了。
public void loadSo(DynamicSoInfo soInfo, ILoadSoListener listener) {
if (soInfo == null) {
return;
}
//根据本机abi,获取适合的动态资源实体类DynamicPkgInfo
DynamicPkgInfo pkg = soInfo.getPkgInfo(Build.SUPPORTED_ABIS);
if (pkg == null) {
return;
}
//如果该so资源,已经被加载缓存过了,直接listener的成功回调,并返回
if (isLoadAndDispatchSo(pkg, listener)) {
return;
}
//开启资源加载,和普通资源流程一致
DynamicResManager manager = DynamicResManager.getInstance();
manager.load(pkg, new DefaultLoadResListener() {
@Override
public void onSucceed(LoadResInfo info) {
super.onSucceed(info);
//so成功下载校验后,执行加载逻辑
handleLoadSoSucceed(pkg, info, listener);
}
});
}
在so资源的加载和应用过程中,我们发现了如下问题
如何判断系统需要哪些so文件,并按需正确加载?
如何下载so文件,并保证它的正确性?
如何将下载的动态so文件,正确应用到系统中?
so资源加载和应用解决方案
如何判断系统需要哪些so文件,并正确下载安装?
我们把arm64-v8a,armeabi-v7a等abi分开打包,上传到服务器。使用时,本地判断abi支持,下载对应的abi包。这样做的优点是节省流量和下载后占据的空间。
至于判断系统需要哪些abi的so包,并按需正确应用,则比较简单,读取系统的SUPPORTED_ABIS常量,这里包含了系统支持的abi列表,而排在前面的表示优先级更高。我们只要遍历它,然后查找我们的动态资源包是否有匹配,就达到了正确加载的目标。
private Map<String,DynamicPkgInfo> mSoInfos;
public DynamicPkgInfo getPkgInfo(){
//获取本地系统支持的abi列表
String[] supportAbis = Build.SUPPORTED_ABIS;
if(supportAbis==null || supportAbis.length== 0 ){
return null;
}
//遍历abi支持列表
for(String abi : supportAbis){
//从so动态资源中,查找对应的abi信息
DynamicPkgInfo pkg = mSoInfos.get(abi);
//找到则直接返回该信息
if(pkg != null){
return pkg;
}
}
return null;
}
复用通过资源加载流程即可。
这里需要首先了解一下,系统加载so库的工作流程,当我们调用 System#loadLibrary("xxx" ) 后,Android Framework 都干了些了啥?Android 的 so 加载机制,大致可以分为以下四个环节。
而我们这里,由于so文件不存在于apk当中,而是需要动态下载,所以我们显然不能直接使用系统的System.loadLibrary方法加载so文件。
而动态加载so的方法,在热修复和插件化框架中,已经比较成熟了,我们参考了市面上的开源框架后,选择了腾讯的Tinker框架的加载方案,即使用反射classloader 将 so 包的路径写入 nativeLibraryPathElements 数组最前面,其流程图和解释如下图所示 。注意,此方法不同的android版本将有不同的实现。下面示例代码基于android9.0版本。
private static void install(ClassLoader classLoader, File soFolder) throws Throwable {
Field pathListField = findField(classLoader, "pathList" );
Object dexPathList = pathListField.get(classLoader);
Field nativeLibraryDirectories = findField(dexPathList, "nativeLibraryDirectories" );
List<File> libDirs = (List<File>) nativeLibraryDirectories.get(dexPathList);
libDirs.add(0, soFolder);
Field systemNativeLibraryDirectories =
findField(dexPathList, "systemNativeLibraryDirectories" );
List<File> systemLibDirs = (List<File>) systemNativeLibraryDirectories.get(dexPathList);
Method makePathElements =
findMethod(dexPathList, "makePathElements" , List.class);
libDirs.addAll(systemLibDirs);
Object[] elements = (Object[]) makePathElements.
invoke(dexPathList, libDirs);
Field nativeLibraryPathElements = findField(dexPathList, "nativeLibraryPathElements" );
nativeLibraryPathElements.setAccessible(true);
nativeLibraryPathElements.set(dexPathList, elements);
}
当外界调用System.loadLibrary方法时,系统最终会调用到DexPathList类的findLibrary方法,该方法会在nativeLibraryPathElements数组中查找对应的路径,我们将自己的so加入到nativeLibraryPathElements最前面,由此达到动态加入so的目标。
解决该问题有如下几种思路:
自定义 System.loadLibrary,加载 SO 前,先解析 SO 的依赖信息,再递归加载其依赖的 SO 文件,这是开源库soLoader的解决方案。
自定义 Linker,完全自己控制 SO 文件的检索逻辑 ,这是开源库Relinder的解决方案。
替换 ClassLoader 。
本着不重复造轮子的原则,项目中使用了Relinker开源库,用来加载so文件。
so库依赖分析工具
想要把 so 动态化技术应用到 APK 的瘦身项目中来,除了分析哪些 so 文件体积占比比较大之外,最好的做法是将其依赖的所有 so 文件一定挪到插件包里。怎么了解 APK 里所有 so 文件具体的依赖信息呢?这里推荐一款 Google 开源的 APK 解析工具android-classyshark,除了提供分析 APK dex/so 依赖信息之外,它还提供了 GUI 可视化界面,非常适合快速上手。
so资源应用流程
获取系统支持abi列表,根据该列表,找到合适的so动态资源实体类。
如果该资源已经被加载缓存,则直接回调加载成功。
否则,开始资源通用加载流程,并异步等待资源加载成功(流程见第5章)。
再次判断下载校验后的资源,是否支持本机abi。
将so包路径加入DexPathList的数组头部。
遍历等待加载so列表,尝试加载所有so文件,并将成功加载的so文件,移除该列表。
将资源id和本地路径加入缓存,防止so被重复加载。
回调加载完成监听器。
从上一章我们知道,我们会使用transform api加asm框架,将系统的System.loadLibrary方法替换成我们的SoloadUtil.loadLibrary方法。我们替换系统方法的目的。一个是为了保证so库不存在时,程序不崩溃,另外一个就是so库下载校验完成后,能自动完成之前失败的加载,为此,我们设计了如下流程。
前面我们已经分析了通用资源加载,内置资源应用,完成了动态资源管理系统的主要部分。只剩下资源打包部分了,而所有资源的打包操作,都由dyanmic_res_plugin插件来完成。为了完成打包功能,我们决定在这个dynamic_res_plugin插件内部,新建3个Task。
所以主流程也就出来了
该task流程,主要就是通过tranform api和asm框架的使用,我们在其中加入了扫描class范围的可配置项。
DeleteAndCopySoTask流程
根据配置文件,找到系统的merge和strip task。
将我们的task插入到2个系统task之间,并等待系统回调我们的doLast方法。
遍历系统的mergeTask的输出目录,判断该so文件是否在我们配置的待扫描列表中。
如果配置了需要拷贝so文件,则我们将它拷贝到指定位置。
如果配置了需要删除so文件,则我们将该so文件删除。
ZipResTask流程
拷贝字体文件,将文件信息加入资源列表。
压缩帧动画文件,将压缩后的文件信息加入资源列表。
压缩so文件,将压缩后的文件信息加入资源列表。
压缩zip文件夹下文件,将压缩后的文件信息加入资源列表。
遍历资源文件,为其生成相应的资源实体类DynamicPkgInfo。
可以与第4章,整体架构图结合起来看。
DynamicPlugin类,实现了系统gradle插件的plugin接口,为我们整个插件的入口,主要解析配置文件,并按照配置文件创建task信息。
DynamicParam类,提供了存储并解析dyanmic_plugin配置文件的方法。
ITask接口,代表了一个我们定义的任务。
DeleteAndCopySoTask,删除并拷贝so文件任务。
TransfomrTask,替换系统System.loadLibrary方法任务。
ZipResTask,压缩so和其他文件,并生成对应的java资源实体类方法。
SystemLoadClassVisitor类,Asm框架的class访问类。
SystemLoadMethodVisitor类,Asm框架的method访问类,用于替换System.loadLibrary方法。
JavaFileCreate类,使用javapoet框架产生java文件。
其他辅助类,在此省略
该配置文件主要包含了配置dynamic_plugin插件运行步骤,插件输入输出路径,so文件扫描路径等信息。
dynamic_config = [
//是否执行替换System.loadlibrary操作
is_replace_load_library: false,
//是否执行替换System.load操作
is_replace_load : false,
//是否执行删除so文件操作
is_delete_so : false,
//是否执行将so文件拷贝到其他目录操作
is_copy_so : false,
//是否执行将动态资源打包,并生成java文件操作
is_zip_res : false,
//是否执行将so文件打包,并生成java文件操作
is_zip_so : false,
//是否自动上传所有资源,上传方法为dynamic_upload
is_upload_res : false,
//插件是否工作在Release模式下
is_release_type : isReleaseBuildType(),
//是否打印debug日志
is_debug_log : true,
//自动创建java文件时的包名
create_java_pkg_name : 'com.test' ,
]
/**
* 配置要删除和拷贝的so文件
* map的key为压缩包名称,值为压缩包包含的so文件列表
* key为debug_all_test时,会压缩所有so包
*/
dynamic_scan_so_map = [
guang_dong : [ 'libpajf.so' , 'libpajf_av.so' , 'libsqlite.so' ],
]
dynamic_so_config = [
//so文件忽略列表,该表中的文件,不会被扫描。不在该列表中的文件都会被扫描
// (dynamic_scan_so_map为空时,本列表才生效)
ignore_so_files: [],
//so文件扫描abi目录,不在该目录下的so将不被扫描
scan_so_abis : [ "arm64-v8a" , "armeabi-v7a" ],
//拷贝出来的so文件夹前缀,ignore_so_files生效时使用
so_input_prefix: 'test' ,
]
dynamic_lib_list = [
//只有该列表中的包名,才会执行替换System.loadlibrary操作
//输入debug_all_test,则会替换所有System.loadLibrary方法,用于测试
scan_load_library_pkgs : [],
//在该列表中的包名或者类名,不会执行替换System.loadlibrary操作,和上面的配置可以同时生效
ignore_load_library_pkgs: [],
]
//该配置不要改动内容,需要改变路径的,直接改变对应的方法内容即可
dynamic_dir = [
//产生文件的输出目录
output : createOrGetOutputPath(),
//字体资源输入目录
typeface_input : createOrGetInputTypafacePath(),
//帧动画资源输入目录
frame_anim_input: createOrGetInputFrameAnimPath(),
//so文件资源输入目录
so_input : createOrGetInputSoPath(),
//zip包输入目录
zip_input : createOrGetInputZipPath()
]
//该配置项,配置了android 2个gradle task的名称
//主工程的mergeNativeLibs合并所有依赖的 native 库
//主工程的stripDebugSymbols从 Native 库中移除 Debug 符号。
dynamic_task = [
//自定义的task运行哪里
//true为mergeNativeLibs之后,stripDebugSymbols之前
//false为stripDebugSymbols之后,package之前
isTaskRunAfterMerge : true,
//debug状态下,mergeNativeLibs的task名称
debugMergeNativeLibs : "mergeDebugNativeLibs" ,
//release状态下,mergeNativeLibs的task名称
releaseMergeNativeLibs : "mergeReleaseNativeLibs" ,
//debug状态下,stripDebugSymbols的task名称
debugStripDebugSymbols : "stripDebugDebugSymbols" ,
//release状态下,stripDebugSymbols的task名称
releaseStripDebugSymbols: "stripReleaseDebugSymbols" ,
//debug状态下,系统打包task名称
debugPackage : "packageDebug" ,
//release状态下,系统打包task名称
releasePackage : "packageRelease" ,
//debug状态下,mergeNativeLibs的输出目录
debugNativeOutputPath : " $ { projectDir }/app/build/intermediates/merged_native_libs/debug/out/lib" ,
//release状态下,mergeNativeLibs的输出目录
releaseNativeOutputPath : " $ { projectDir }/app/build/intermediates/merged_native_libs/release/out/lib" ,
//debug状态下,,stripDebugSymbols的输出目录
debugStripOutputPath : " $ { projectDir }/app/build/intermediates/stripped_native_libs/debug/out/lib" ,
//release状态下,,stripDebugSymbols的输出目录
releaseStripOutputPath : " $ { projectDir }/app/build/intermediates/stripped_native_libs/release/out/lib" ,
]
//该闭包可以自动将文件上传到服务器,参数列表为资源id,资源文件路径
//我们可以再次执行上传服务器操作,并返回对应的url。
//当然也可以不实现上传操作,并自己手动上传资源。
dynamic_upload = {
id, path ->
println( "dynamic_upload id $ { id } ,path $ { path }" )
return 'http://url'
}
通过引入动态资源管理系统,并将一键报警sdk相关的so文件和其他普通资源动态化后,货拉拉用户端的包体积减少了8M,从54M变为了46M。后继将会继续尝试进行其他so文件的动态化。
作者:货拉拉技术 链接:https://juejin.cn/post/7238627611265613882
本文由哈喽比特于1年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/62lzaxsH9E6Dst5XlK7NLw
京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。
日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。
据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。
今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。
日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。
近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。
据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。
9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...
9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。
据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。
特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。
据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。
近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。
据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。
9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。
《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。
近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。
社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”
2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。
罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。