万字长文深度解析JDK序列化原理及Fury高度兼容的极致性能实现

发表于 1年以前  | 总阅读数:530 次

Fury是一个基于JIT动态编译的高性能多语言原生序列化框架,支持Java/Python/Golang/C++/JavaScript等语言,提供全自动的对象多语言/跨语言序列化能力,以及相比于别的框架最高20~200倍的性能。

序言

对于Java对象序列化,由于JDK自带的序列化性能很差,业界出现了hessian/kryo等框架来加速序列化。这些框架能够序列化大部分Java对象,但如果对象实现了writeObject/readObject/ writeReplace/readResolve等JDK自定义序列化方法 ,这些框架便无能为力。由于用户可能在这些方法当中执行任意逻辑,为了保证序列化的正确性,这些方法需要被以符合JDK序列化的行为方式被执行,这时候用户只能选择JDK自带的序列化框架,忍受极其缓慢的性能。

而业务系统的数据对象自定义JDK序列化是很常见的事情,比如下方是某个复杂场景序列化使用Fury测试下来的火焰图,里面就有相当一部分开销在JDK序列化上面(Fury早期版本在遇到自定义JDK序列化的类型时会转发给JDK进行序列化)。

为了提高序列化的性能,保证任意场景不回退,Fury从0.9.2版本开始完整实现了整套JDK序列化协议,兼容所有JDK自定义序列化行为,从而在任意场景避免使用JDK序列化,保证高效的序列化性能。

本文将首先分析JDK序列化原理,接下来基于JDK序列化原理展开hessian/kryo等框架的不足之处,然后介绍Fury的高效兼容实现,最后给出性能对比的数据。

JDK序列化原理分析

JDK序列化框架使用ObjectOutputStream和ObjectInputStream序列化和反序列化,该框架允许用户通过:

Externalizable/writeObject/readObject/readObjectNoData/writeReplace/readResolve

等方法来自定义序列化的行为。当要序列化的对象不包含这些方法时,ObjectOutputStream会调用内部的defaultWriteObject来序列化类型层次结构的所有字段和类型信息,反序列化时会使用ObjectInputStream来读取类型层次结构的每个类型相关信息和对应每个字段值并填充整个对象。如果包含自定义序列化方法,则需要走到单独的执行流程。

序列化整体流程

当对象定义了writeReplace方法时,序列化会先调用该方法,然后使用该方法返回的对象引用取代引用表之前记录的引用。如果返回对象类型不变,即返回类型仍有writeReplace方法,这时候该方法会被忽略,进入正常的writeObject/writeExternal流程。如果返回类型发生变化,则循环调用writeReplace方法重复前述流程。

当返回对象不再包含writeReplace方法时,这时候便进入到字段数据序列化的过程,如果对象实现了Externalizable接口,则调用writeExternal进行序列化,否则从对象层次结构的第一个定义了Serializable的父类开始,依次序列化每个类型以及属于当前类型的所有字段数据。

当对象层次结构的某个类型定义了writeObject方法时,对于对应到该类型的字段的序列化,则会调用调用该类型定义的writeObject方法进行,writeObject方法内部可以调用ObjectOutputStream的defaultWriteObject完成默认字段的序列化,或者完全手写序列化逻辑。

对于不同JDK版本字段不一致需要兼容的情况,则需要调用putFields方法获取PutField对象,用于将已知字段和只在某些JDK版本存在但当前JDK版本不存在的字段数据填充到该对象,然后调用writeFields完成字段数据的写入。

比如ThreadLocalRandom就是通过putFields来自定义序列化逻辑:

   private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException {

        java.io.ObjectOutputStream.PutField fields = s.putFields();
        fields.put("rnd", U.getLong(Thread.currentThread(), SEED));
        fields.put("initialized", true);
        s.writeFields();
    }

需要注意defaultWriteObject写的数据可能会通过readFields进行读取,因此其格式需要和和putFields兼容。另外在自定义序列化时defaultWriteObject/putFields两者只能调用一个。

整体流程如下图:

反序列化整体流程

反序列化首先会读取对象类型,然后查询该类型的无参构造函数用于创建对象,如果不存在无参数构造函数,则通过:

ReflectionFactory#newConstructorForSerialization(java.lang.Class<?>)

向上遍历类型层次结构直到获取到第一个非Serializable父类的无参构造函数(该过程会进行缓存,避免重复查找)。

然后根据构造函数创建对象,并将对象放入引用表,避免循环引用找不到对象。

接下来从第一个Serializable父类开始依次反序列化每个类型和对应的字段数据,并填充到之前通过构造函数创建的对象里面。如果某个反序列化的类型不存在,则代表对象层次结构发生了变化,反序列化端对象增加了新的父类,如果该类型定义了readObjectNoData方法,则会调用该方法初始化字段状态,否则这部分字段将出于默认状态。

如果父类类型没有定义readObject,则会通过调用defaultReadObject来依次读取每个非transient非static字段的值并填充到对象里面。如果定义了readObject方法,则调用该方法完成该类型数据的反序列化。

readObject方法可以调用defaultReadObject来完成默认字段值的反序列化,然后执行其它自定义逻辑,或者完全手写反序列化逻辑。

对于不同JDK版本字段不一致需要兼容的情况,则需要调用readFields方法获取GetField对象,该对象可能包含当前Class版本没有的字段数据,这时候可以直接忽略掉,其它字段可以从GetField里面查询出来并设置到对象上面。需要注意defaultReadObject和readFields两者只能调用一个。

某些情况下父类字段的反序列化依赖子类字段反序列化后的状态,由于父类字段先反序列化,这时候无法获取子类反序列化后的状态,因此JDK提供了registerValidation回调来在整个对象完成反序列化后执行,这时可以执行额外的操作恢复对象的状态。

在对象完成序列化之后,检查对象所在类型是否定义了readResolve方法,如果定义了该方法,则调用该方法返回替代对象,如果返回类型发生变化,则循环调用readResolve方法重复前述流程。

在执行完readResolve之后,整个对象便完成了反序列化。

Hessian/Kryo等框架存在的问题

Hessian存在的问题

Hessian目前支持writeReplace/readResolve自定义方法,当对象定义了writeReplace方法时,会通过com.caucho.hessian.io.WriteReplaceSerializer进行序列化。该序列化器能够满足部分场景需求,但当writeReplace方法返回相同类型的新对象时,hessian会出现栈溢出:


public static class CustomReplaceClass implements Serializable {
  Object writeReplace() {
    return new CustomReplaceClass();
  }

  Object readResolve() {
    return new CustomReplaceClass();
  }
}
Exception in thread "main" java.lang.StackOverflowError
  at java.base/java.lang.reflect.InvocationTargetException.<init>(InvocationTargetException.java:73)
  at jdk.internal.reflect.GeneratedMethodAccessor1.invoke(Unknown Source)
  at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
  at java.base/java.lang.reflect.Method.invoke(Method.java:566)
  at com.caucho.hessian.io.WriteReplaceSerializer.writeReplace(WriteReplaceSerializer.java:184)
  at com.caucho.hessian.io.WriteReplaceSerializer.writeObject(WriteReplaceSerializer.java:155)
  at com.caucho.hessian.io.Hessian2Output.writeObject(Hessian2Output.java:465)
  at com.caucho.hessian.io.WriteReplaceSerializer.writeObject(WriteReplaceSerializer.java:167)
  at com.caucho.hessian.io.Hessian2Output.writeObject(Hessian2Output.java:465)
  at com.caucho.hessian.io.WriteReplaceSerializer.writeObject(WriteReplaceSerializer.java:167)
  at com.caucho.hessian.io.Hessian2Output.writeObject(Hessian2Output.java:465)
  at com.caucho.hessian.io.WriteReplaceSerializer.writeObject(WriteReplaceSerializer.java:167)
  at com.caucho.hessian.io.Hessian2Output.writeObject(Hessian2Output.java:465)

Hessian目前不支持writeObject/readObject方法,当要序列化的对象定义了这些方法时,hessian会直接忽略掉,而在实际场景中很多对象都定义了这两个方法,JDK大部分类型也都定义了这两个方法,导致hessian在序列化这些类型时出现状态不一致的错误。

一般在这些类型里面,数据字段一般标记为transient,因此忽略这两个方法直接序列化所有非transient字段会导致数据丢失,比如LinkedBlockingQueue的主要数据字段都全部是transient,在writeObject里面进行特殊的处理:

   /**
     * Head of linked list.
     * Invariant: head.item == null
     */
    transient Node<E> head;

    /**
     * Tail of linked list.
     * Invariant: last.next == null
     */
    private transient Node<E> last;

    private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException {

        fullyLock();
        try {
            // Write out any hidden stuff, plus capacity
            s.defaultWriteObject();

            // Write out all elements in the proper order.
            for (Node<E> p = head.next; p != null; p = p.next)
                s.writeObject(p.item);

            // Use trailing null as sentinel
            s.writeObject(null);
        } finally {
            fullyUnlock();
        }
    }

同时由于没有执行这两个方法里面的自定义逻辑,最终反序列化的对象状态也会不对。比如Java java.util.concurrent.locks.AbstractQueuedLongSynchronizer的子类都需要自定义readObject方法来重设lock状态:


private void readObject(java.io.ObjectInputStream s)
    throws java.io.IOException, ClassNotFoundException {
    s.defaultReadObject();
    readHolds = new ThreadLocalHoldCounter();
    setState(0); // reset to unlocked state
}

对于常见类型,或许可以通过内置序列化器来进行序列化,但这无法枚举所有已知类型和未知类型,一旦出现序列化错误,比如多线程Lock状态错误,将极其难以排查。

同时hessian不支持父子类出现重名字段,这在某些条件下也会成为一个使用限制。

因此在RPC框架里面,很多场景用户会直接选择JDK序列化,这些场景现在全部都可以切换为FURY进行加速。

Kryo存在的问题

Kryo为了保证序列化的正确性,在遇到定义了writeObject/readObject/readObjectNoData/writeReplace/ readResolve的对象时,会调用JDK的ObjectOutputStream和ObjectInputStream进行序列化。该方式存在三个问题:

  • JDK序列化性能很差,导致kryo序列化性能大幅退化
  • JDK序列化结果很大,导致kryo序列化数据膨胀
  • 转发给JDK序列化的对象子图不会跟Kryo share同一个引用表,如果该子图共享/循环引用了其它对象,则会出现重复序列化/递归栈溢出

kryo不支持父子类出现重名字段,这在某些条件下也会成为一个使用限制。

其它框架存在的问题

  • Jsonb不支持任何JDK自定义序列化方法,反序列化会报错
  • Fst不支持类型前后兼容,无法在服务化场景使用

Fury兼容实现原理

早期序列化流程

Fury早期版本序列化流程跟Kryo类型,在遇到writeObject/readObject/readObjectNoData/writeReplace/ readResolve的对象时,调用JDK的ObjectOutputStream和ObjectInputStream进行序列化。

新版序列化流程

在Fury 0.9.2版本,我们提供了一套基于JIT动态编译的100%兼容JDK自定义序列化的实现,性能数量级提升。

整体实现流程模拟了JDK序列化的过程,但实现上使用了Fury内置的JIT序列化器来进行加速和减少序列化结果大小,同时对于对象层次结构的每个Serializable class,只序列化类名称,不序列化类的元数据,减少开销。

整体实现在:

io.fury.serializers.ReplaceResolveSerializer和

io.fury.serializers.ObjectStreamSerializer

两个序列化器里面,分别负责writeReplace/readResolve自定义序列化和writeObject/readObject/readObjectNoData自定义序列化。

ReplaceResolveSerializer

ReplaceResolveSerializer完整实现了JDK相同的replace/resolve行为,即使在writeReplace方法返回相同类型不同引用的对象,也能够正常序列化,不会出现hessian一下的栈溢出问题。同时在返回对象类型跟原始对象类型不同时,fury可以避免写入原始对象的类名称,减少序列化的结果大小。

如果对象同时定义了writeObject/readObject/readObjectNoData/writeReplace/readResolve方法,fury会分发给ReplaceResolveSerializer处理引用replace/resolve,将处理完之后的对象再交给ObjectStreamSerializer进行JDK自定义序列化流程。

ObjectStreamSerializer

ObjectStreamSerializer实现了整套:

JDK writeObject/readObject/readObjectNoData/registerValidation

行为,保证行为跟JDK的一致性,在任意情况下序列化都不会报错。

由于用户在:

writeObject/readObject/ readObjectNoData/registerValidation

里面调用的是:

JDK ObjectOutputStream/ObjectInputStream /PutField/GetField的接口

因此Fury也实现了一套:

ObjectOutputStream/ObjectInputStream/PutField/ GetField

的子类,保证实际序列化逻辑可以转发给Fury。

为了保证类型前后兼容,同时保证:

defaultWriteObject/defaultReadObject

跟putFields/readFields的兼容性,字段数据序列化使用的是Fury的CompatibleSerializer,在读写端类型不一致的情况下也可以争取反序列化。

为了保证高性能,在开启JIT模式时会通过:

io.fury.serializers.CodegenSerializer#loadCompatibleCodegenSerializer创建JITCompatibleSerializer进行序列化。

整体实现分为序列化器初始化部分和执行部分。

序列化器初始化部分

  • 获取无参数构造函数或者第一个Non Serializable父类无参数构造函数。为了避免JDK17以上版本的反射访问权限问题,在JDK17以上版本会通过Unsafe直接获取ObjectStreamClass.lookup(type)抽取的构造函数。
   Constructor constructor;
    try {
      constructor = type.getConstructor();
      if (!constructor.isAccessible()) {
        constructor.setAccessible(true);
      }
    } catch (Exception e) {
      constructor =
          (Constructor) ReflectionUtils.getObjectFieldValue(ObjectStreamClass.lookup(type), "cons");
    }
  • 遍历对象层次结构,创建每个class的JITCompatibleSerializer/CompatibleSerializer/ FuryObjectOutputStream/FuryObjectInputStream等。其中slotsSerializer是JITCompatibleSerializer,用于执行ObjectOutputStream#defaultWriteObject/ObjectInputStream#defaultReadObject。compatibleStreamSerializer用于序列化将PutFields设置的字段,因为PutFields设置的字段可能在当前对象类型里面不存在,因此这种情况下无法使用JIT进行类型推断和序列化。
   List<SlotsInfo> slotsInfoList = new ArrayList<>();
    Class<?> end = type;
    // locate closest non-serializable superclass
    while (end != null && Serializable.class.isAssignableFrom(end)) {
      end = end.getSuperclass();
    }
    while (type != end) {
      slotsInfoList.add(new SlotsInfo(fury, type));
      type = type.getSuperclass();
    }
    Collections.reverse(slotsInfoList);
    slotsInfos = slotsInfoList.toArray(new SlotsInfo[0]);

对象层次结构的每个类型的序列化相关信息:


  private static class SlotsInfo {
    private final Class<?> cls;
    private final Method writeObjectMethod;
    private final Method readObjectMethod;
    private final Method readObjectNoData;
    private final CompatibleSerializerBase slotsSerializer;
    private final ObjectIntMap<String> fieldIndexMap;
    private final FieldResolver putFieldsResolver;
    private final CompatibleSerializer compatibleStreamSerializer;
    private final FuryObjectOutputStream objectOutputStream;
    private final FuryObjectInputStream objectInputStream;
    private final ObjectArray getFieldPool;
  }

序列化器执行部分

  • 序列化执行部分

  • 写入所有Serializable class数量

  • 遍历对象类层次结构,依次序列化每个类型的字段数据。序列化每个类型的数据分为以下几个部分:

  • 如果当前对象所在类型没有定义writeObject方法,则直接调用slotsSerializer (JITCompatibleSerializer)序列化当前类型所有字段。

  • 如果前对象所在类型定义了writeObject方法,则会缓存之前一次序列化的上下文,然后调用writeObject方法,传入Fury实现的FuryObjectOutputStream。

  • 在FuryObjectOutputStream里面,同时也会针对putFields/writeFields/defaultWriteObject进行特殊的处理。putFields/writeFields会把对象转换成CompatibleSerializer可识别的array形式,defaultWriteObject则会直接调用slotsSerializer (JITCompatibleSerializer)序列化当前类型所有字段。

整体代码流程如下:


for (SlotsInfo slotsInfo : slotsInfos) {
    buffer.writeShort((short) slotsInfos.length);
    classResolver.writeClassInternal(buffer, slotsInfo.cls);
    Method writeObjectMethod = slotsInfo.writeObjectMethod;
    if (writeObjectMethod == null) {
      slotsInfo.slotsSerializer.write(buffer, value);
    } else {
      FuryObjectOutputStream objectOutputStream = slotsInfo.objectOutputStream;
      Object oldObject = objectOutputStream.targetObject;
      MemoryBuffer oldBuffer = objectOutputStream.buffer;
      FuryObjectOutputStream.PutFieldImpl oldPutField = objectOutputStream.curPut;
      boolean fieldsWritten = objectOutputStream.fieldsWritten;
      try {
        objectOutputStream.targetObject = value;
        objectOutputStream.buffer = buffer;
        objectOutputStream.curPut = null;
        objectOutputStream.fieldsWritten = false;
        writeObjectMethod.invoke(value, objectOutputStream);
      } finally {
        objectOutputStream.targetObject = oldObject;
        objectOutputStream.buffer = oldBuffer;
        objectOutputStream.curPut = oldPutField;
        objectOutputStream.fieldsWritten = fieldsWritten;
      }
    }
  }

反序列化执行部分:

  • 根据构造函数创建对象实例。

  • 将对象实例写入引用表。

  • 读取对象层次结构所有Serializable Class数量。

  • 依次从数据读取class,并和当前类型层次结构的class进行比较。如果不一致,则代表当前类型层次结构发生了变化,引入了新的父类,如果该类型定义了readObjectNoData,则调用该方法进行初始化,然后向上遍历类型层次结构,直到找到相同类型。

  • 反序列化该类型的所有字段值并设置到对象字段上面。

  • 如果对象没有定义readObject方法,则直接调用slotsSerializer (JITCompatibleSerializer)进行反序列化。

  • 如果定义了readObject方法,则调用对象的readObject方法,传入Fury实现的FuryObjectInputStream。

  • 在FuryObjectInputStream里面,同时也会针对readFields/defaultReadObject进行特殊的处理。readFields会使用CompatibleSerializer把对象转换成可识别的GetField形式,defaultReadObject则会直接调用slotsSerializer (JITCompatibleSerializer)反序列化当前类型所有字段。

  • 如果在readObject期间用户通过registerValidation注册了ObjectInputValidation回调,则会在返回该对象之前,按照优先级依次执行回调。

  • 至此反序列化完成。核心代码大致如下:

    Object obj = null;
    if (constructor != null) {
      try {
        obj = constructor.newInstance();
      } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) {
        Platform.throwException(e);
      }
    } else {
      obj = Platform.newInstance(type);
    }
    fury.getReferenceResolver().reference(obj);
    int numClasses = buffer.readShort();
    int slotIndex = 0;
    TreeMap<Integer, ObjectInputValidation> callbacks = new TreeMap<>(Collections.reverseOrder());
    for (int i = 0; i < numClasses; i++) {
      Class<?> currentClass = classResolver.readClassInternal(buffer);
      SlotsInfo slotsInfo = slotsInfos[slotIndex++];
      while (currentClass != slotsInfo.cls) {
        // the receiver's version extends classes that are not extended by the sender's version.
        Method readObjectNoData = slotsInfo.readObjectNoData;
        if (readObjectNoData != null) {
          readObjectNoData.invoke(obj);
        }
        slotsInfo = slotsInfos[slotIndex++];
      }
      Method readObjectMethod = slotsInfo.readObjectMethod;
      if (readObjectMethod == null) {
        slotsInfo.slotsSerializer.readAndSetFields(buffer, obj);
      } else {
        FuryObjectInputStream objectInputStream = slotsInfo.objectInputStream;
        MemoryBuffer oldBuffer = objectInputStream.buffer;
        Object oldObject = objectInputStream.targetObject;
        FuryObjectInputStream.GetFieldImpl oldGetField = objectInputStream.getField;
        FuryObjectInputStream.GetFieldImpl getField =
            (FuryObjectInputStream.GetFieldImpl) slotsInfo.getFieldPool.popOrNull();
        if (getField == null) {
          getField = new FuryObjectInputStream.GetFieldImpl(slotsInfo);
        }
        boolean fieldsRead = objectInputStream.fieldsRead;
        try {
          objectInputStream.fieldsRead = false;
          objectInputStream.buffer = buffer;
          objectInputStream.targetObject = obj;
          objectInputStream.getField = getField;
          objectInputStream.callbacks = callbacks;
          readObjectMethod.invoke(obj, objectInputStream);
        } finally {
          objectInputStream.fieldsRead = fieldsRead;
          objectInputStream.buffer = oldBuffer;
          objectInputStream.targetObject = oldObject;
          objectInputStream.getField = oldGetField;
          slotsInfo.getFieldPool.add(getField);
          objectInputStream.callbacks = null;
          Arrays.fill(getField.vals, FuryObjectInputStream.NO_VALUE_STUB);
        }
      }
    }
    for (ObjectInputValidation validation : callbacks.values()) {
      validation.validateObject();
    }

性能对比

在完整实现JDK自定义序列化后,Fury在任意场景下都不再会调用到JDK序列化里面。Fury在没有做任何配置的情况下,在同样数据测试下来,相比Kryo有10倍的性能提升,相比hessian等框架也有3倍的性能提升(大部分数据为string和hashmap,string序列化和hashmap iteration/rebalance摊平了JIT的优势)。

下方是Fury序列化时的火焰图,也可以清晰看到里面已经没有JDK序列化的stack了:

结论

可以看到,从0.9.2版本开始,fury在任意场景相比JDK/Kryo/Hessian都有显著的性能优势,同时也是业界唯一一个能够跟JDK序列化保持100%兼容性和正确性的框架。目前由于hessian的正确性问题,很多业务都会直接使用JDK序列化,忍受其缓慢的性能,我们希望通过Fury提供的序列化能力,彻底告别JDK序列化,将业务从这种痛苦当中解放出来,提供更高的生产力。

目前Fury在Java序列化这块的建设已经相当完善,接下来我们会开源Fury,如果有开源使用场景或者合作意向,欢迎通过邮箱chaokun.yck@antgroup.com 交流。

参考链接:

[1]writeObject:https://docs.oracle.com/en/java/javase/18/docs/specs/serialization/output.html#the-writeobject-method

[2]readObject:

https://docs.oracle.com/en/java/javase/18/docs/specs/serialization/input.html#the-readobject-method

[3]writeReplace:

https://docs.oracle.com/en/java/javase/18/docs/specs/serialization/output.html#the-writereplace-method

[4]readResolve:https://docs.oracle.com/en/java/javase/18/docs/specs/serialization/input.html#the-readresolve-method

[5]Externalizable:https://docs.oracle.com/en/java/javase/18/docs/specs/serialization/output.html#the-writeexternal-method

[6]readObjectNoData:https://docs.oracle.com/en/java/javase/18/docs/specs/serialization/input.html#the-readobjectnodata-method

[7]registerValidation:https://docs.oracle.com/en/java/javase/18/docs/specs/serialization/input.html

本文由哈喽比特于1年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/6YJlfOpXjkr-9pUGoxphVA

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237227次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8063次阅读
 目录