微信 Mars Android Sample 源码分析

2023-05-16

注:原文首发地址

零、前言

Mars 是微信官方开源的跨平台跨业务的终端基础组件,具有高质量网络连接模块(长短连接、智能心跳机制)、高性能日志模块和网络监测组件等。而整个 Android Sample 是基于 Mars 开发的一个 demo,包含提供了以下功能:

  • 基于TCP长连接的聊天室体验。
  • 数据通信成功率、耗时、流量的展示。
  • 网络状况检测结果展示。

一、本地运行 Server 端

具体如何运行 Server 端,参照官方wiki:Mars Sample 使用说明

二、修改 Android Sample

下面说下Android 端该如何修改源码连接到本地服务器

  • 1.全局搜索marsopen.cn,修改替换为localhost
  • 2.在保证 app/build.gradleuseLocalMarsWrapper = true的情况下,在 wrapper module 下修改 com.tencent.mars.sample.wrapper.service. MarsServiceStub.java 的 dns 解析的地址为本地主机的 IP 地址 (wiki 上没写清楚)
    @Override
    public String[] onNewDns(String host) {
        // No default new dns support
        return new String[]{"192.168.24.193"}; //替换为本地主机的 IP 地址
    }

三、运行后聊天效果

IM聊天

终端日志

四、整体概述

  1. 由项目结构可知 sample 分为两部分,app 和 wrapper(当然也可以用依赖的方式来使用),数据格式使用的是google开源的 protobuf,它具有高效、数据量小的特性(目前版本为 proto3,Android 开发推荐使用 protobuf-lite 版本,使用方法)

  2. Mars Android 接入指南 · Tencent/mars Wiki · GitHub有这样一条介绍:

    目前 gradle 接入支持两种方式:mars-core 和 mars-wrapper。只是做个 sample 的话建议可以使用 mars-wrapper, 但是如果在实际 App 中使用 mars,建议使用 mars-core 或本地编译。
    怎么理解这句话?两种接入有什么不同?
    从 sample 中 mars-wrapper(下面简称为 wrapper) 的源码可以看出 wrapper 只是对 mars-core 进行了再次封装,wrapper 库本质还是依赖 mars-core 库的,所以微信团队不建议实际App 中接入 wrapper ( 它只是微信官方提供对 mars-core 使用的一种方式,不过具有很好的参考价值)

  3. 另外,有 wrapper 的 manifest 可知,IM 服务模块是运行在独立的进程的,所以同时会涉及到 AIDL 多进程通信方面技术。

 <application>
        <service
            android:name=".service.MarsServiceNative"
            android:process=":marsservice" />

        <!--注册一个网络切换的广播接收器(源码上实现的)-->
        <receiver
            android:name="com.tencent.mars.BaseEvent$ConnectionReceiver"
            android:process=":marsservice" />
    </application>

五、详细分析

1. 先从 ConversationActivity 获取初始的会话列表的请求开始

      private NanoMarsTaskWrapper<Main.ConversationListRequest, Main.ConversationListResponse> taskGetConvList = null;

    private void updateConversationTopics() {
        if (taskGetConvList != null) {
            MarsServiceProxy.cancel(taskGetConvList);
        }

        textView.setVisibility(View.INVISIBLE);
        progressBar.setVisibility(View.VISIBLE);

        swipeRefreshLayout.setRefreshing(true);

        taskGetConvList = new NanoMarsTaskWrapper<Main.ConversationListRequest, Main.ConversationListResponse>(
                new Main.ConversationListRequest(),
                new Main.ConversationListResponse()
        ) {

            private List<Conversation> dataList = new LinkedList<>();

            @Override
            public void onPreEncode(Main.ConversationListRequest req) {
                req.type = conversationFilterType;
                req.accessToken = ""; // TODO:
            }

            @Override
            public void onPostDecode(Main.ConversationListResponse response) {

            }

            @Override
            public void onTaskEnd(int errType, int errCode) {
                runOnUiThread(new Runnable() {

                    @Override
                    public void run() {
                        if (response != null) {
                            for (Main.Conversation conv : response.list) {
                                dataList.add(new Conversation(conv.name, conv.topic, conv.notice));
                            }
                        }

                        if (!dataList.isEmpty()) {
                            progressBar.setVisibility(View.INVISIBLE);
                            conversationListAdapter.list.clear();
                            conversationListAdapter.list.addAll(dataList);
                            conversationListAdapter.notifyDataSetChanged();

                            swipeRefreshLayout.setRefreshing(false);

                        } else {
                            Log.i(TAG, "getconvlist: empty response list");
                            progressBar.setVisibility(View.INVISIBLE);
                            textView.setVisibility(View.VISIBLE);
                        }
                    }
                });
            }

        };

        MarsServiceProxy.send(taskGetConvList.setHttpRequest(CONVERSATION_HOST, "/mars/getconvlist"));
    }

执行步骤:

  • 创建一个 NanoMarsTaskWrapper 对象,里面主要包含 onPreEncode,onPostDecode 和onTaskEnd 等方法,分别是编码传输前,接收数据解码后和任务结束后的回调;
  • 设置 NanoMarsTaskWrapper 的 http 地址
  • 通过 MarsServiceProxy.send 方法执行发送请求;

初步了解执行步骤后,再详细了解 MarServiceProxy 和 NanoMarTaskWrapper 的实现,它们为什么会有这样的功能。

2. NanoMarTaskWrapper

顾名思义,NanoMarTaskWrapper 是一个任务的包装器

public abstract class NanoMarsTaskWrapper<T extends MessageNano, R extends MessageNano> extends AbstractTaskWrapper {

    private static final String TAG = "Mars.Sample.NanoMarsTaskWrapper";

    protected T request;
    protected R response;

    public NanoMarsTaskWrapper(T req, R resp) {
        super();

        this.request = req;
        this.response = resp;
    }

    @Override
    public byte[] req2buf() {
        try {
            onPreEncode(request);

            final byte[] flatArray = new byte[request.getSerializedSize()];
            final CodedOutputByteBufferNano output = CodedOutputByteBufferNano.newInstance(flatArray);
            request.writeTo(output);

            Log.d(TAG, "encoded request to buffer, [%s]", MemoryDump.dumpHex(flatArray));

            return flatArray;

        } catch (Exception e) {
            e.printStackTrace();
        }

        return new byte[0];
    }

    @Override
    public int buf2resp(byte[] buf) {
        try {
            Log.d(TAG, "decode response buffer, [%s]", MemoryDump.dumpHex(buf));

            response = MessageNano.mergeFrom(response, buf);
            onPostDecode(response);
            return StnLogic.RESP_FAIL_HANDLE_NORMAL;

        } catch (Exception e) {
            Log.e(TAG, "%s", e);
        }

        return StnLogic.RESP_FAIL_HANDLE_TASK_END;
    }

    public abstract void onPreEncode(T request);

    public abstract void onPostDecode(R response);
}
  • 1)继承自AbstractTaskWrapper
  • 2)创建时会同时创建继承自 MessageNano(protobuf 的消息数据类) 的 request 和 response 数据模型;
  • 3)处理编解码的两个方法,req2buf 和 buf2resp,也就是字节流数组和 protobuf 对象的转换,涉及到 MessageNano.writeToMessageNano.mergeFrom的使用,这方面的具体实现不需要我们去了解,Google 的 protobuf 已经帮我们生成代码完成好了

再看看 AbstractTaskWrapper 是怎样实现的

public abstract class AbstractTaskWrapper extends MarsTaskWrapper.Stub {

    private Bundle properties = new Bundle();

    public AbstractTaskWrapper() {

        // Reflects task properties 通过自身(继承该类的类)标注的注解,用反射获取任务的配置信息
        final TaskProperty taskProperty = this.getClass().getAnnotation(TaskProperty.class);
        if (taskProperty != null) {
            setHttpRequest(taskProperty.host(), taskProperty.path());
            setShortChannelSupport(taskProperty.shortChannelSupport());
            setLongChannelSupport(taskProperty.longChannelSupport());
            setCmdID(taskProperty.cmdID());
        }
    }

    @Override
    public Bundle getProperties() {
        return properties;
    }

    @Override
    public abstract void onTaskEnd(int errType, int errCode);

    public AbstractTaskWrapper setHttpRequest(String host, String path) {
        properties.putString(MarsTaskProperty.OPTIONS_HOST, ("".equals(host) ? null : host));
        properties.putString(MarsTaskProperty.OPTIONS_CGI_PATH, path);

        return this;
    }

    public AbstractTaskWrapper setShortChannelSupport(boolean support) {
        properties.putBoolean(MarsTaskProperty.OPTIONS_CHANNEL_SHORT_SUPPORT, support);
        return this;
    }

    public AbstractTaskWrapper setLongChannelSupport(boolean support) {
        properties.putBoolean(MarsTaskProperty.OPTIONS_CHANNEL_LONG_SUPPORT, support);
        return this;
    }

    public AbstractTaskWrapper setCmdID(int cmdID) {
        properties.putInt(MarsTaskProperty.OPTIONS_CMD_ID, cmdID);
        return this;
    }

    @Override
    public String toString() {
        return "AbsMarsTask: " + BundleFormat.toString(properties);
    }
}

抽象的 AbstractTaskWrapper 继承自 MarTaskWrapper.Stub (MarsTaskWrapper.aidl 生成的代码),它主要通过注解类 TaskProperty 设置了任务的配置信息 properties(如主机名、url路径、是否支持长短链接和指令 id),

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Inherited
public @interface TaskProperty {

    String host() default "";

    String path();

    boolean shortChannelSupport() default true;

    boolean longChannelSupport() default false;

    int cmdID() default -1;
}

再回到刚开始的发送消息的 MarsServiceProxy 类

3. MarsServiceProxy

public class MarsServiceProxy implements ServiceConnection {
      //……
    private MarsService service = null;  
    public static MarsServiceProxy inst;
    private LinkedBlockingQueue<MarsTaskWrapper> queue = new LinkedBlockingQueue<>();
      private MarsServiceProxy() {
        worker = new Worker();
        worker.start();
    }

    public static void init(Context context, Looper looper, String packageName) {
        if (inst != null) {
            // TODO: Already initialized
            return;
        }

        gContext = context.getApplicationContext();

        gPackageName = (packageName == null ? context.getPackageName() : packageName);
        gClassName = SERVICE_DEFAULT_CLASSNAME;

        inst = new MarsServiceProxy();
    }

    @Override
    public void onServiceConnected(ComponentName componentName, IBinder binder) {
        Log.d(TAG, "remote mars service connected");

        try {
            service = MarsService.Stub.asInterface(binder);
            service.registerPushMessageFilter(filter);
            service.setAccountInfo(accountInfo.uin, accountInfo.userName);

        } catch (Exception e) {
            service = null;
        }
    }
      //……
    private static class Worker extends Thread {

        @Override
        public void run() {
            while (true) {
                inst.continueProcessTaskWrappers();
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    //
                }
            }
        }
    }
}

MarsServiceProxy 只是一个单例的 ServiceConnection,不过它应该具有代理某种 Service的功能(ServiceConnection会和 Service绑定),由定义的变量和onServiceConnected中的调用可知,它关联了 MarsService,但是 MarsService 只是一个 AIDL ,不是真正的服务,这个稍后再说
MarsServiceProxy 是在SampleApplication 的 onCreate调用的时候初始化的

// NOTE: MarsServiceProxy is for client/caller
// Initialize MarsServiceProxy for local client, can be moved to other place
MarsServiceProxy.init(this, getMainLooper(), null);

当调用send 方法发送任务时,

    public static void send(MarsTaskWrapper marsTaskWrapper) {
        inst.queue.offer(marsTaskWrapper);
    }

queue 是一个 LinkedBlockingQueue线程安全的队列,缓存了所有的MarsTaskWrapper 任务。
那将任务放进队列后,什么时候执行呢?由上面代码可以看到 MarsServiceProxy 创建时会启动一个 Worker 线程,线程会每隔50ms 执行调用inst.continueProcessTaskWrappers();方法

     private void continueProcessTaskWrappers() {
        try {
            if (service == null) {  //服务为 null时,重新开启
                Log.d(TAG, "try to bind remote mars service, packageName: %s, className: %s", gPackageName, gClassName);
                Intent i = new Intent().setClassName(gPackageName, gClassName);
                gContext.startService(i);
                if (!gContext.bindService(i, inst, Service.BIND_AUTO_CREATE)) {  //注意,此处有个 Service 绑定的判断
                    Log.e(TAG, "remote mars service bind failed");
                }

                // Waiting for service connected
                return;
            }

            MarsTaskWrapper taskWrapper = queue.take(); //取出队列中的 MarsTask
            if (taskWrapper == null) { //任务为空,跳出方法的执行
                // Stop, no more task
                return;
            }

            try {
                Log.d(TAG, "sending task = %s", taskWrapper);
                final String cgiPath = taskWrapper.getProperties().getString(MarsTaskProperty.OPTIONS_CGI_PATH);
                final Integer globalCmdID = GLOBAL_CMD_ID_MAP.get(cgiPath);   
                if (globalCmdID != null) {
                    taskWrapper.getProperties().putInt(MarsTaskProperty.OPTIONS_CMD_ID, globalCmdID);
                    Log.i(TAG, "overwrite cmdID with global cmdID Map: %s -> %d", cgiPath, globalCmdID);
                }

                final int taskID = service.send(taskWrapper, taskWrapper.getProperties());
                // NOTE: Save taskID to taskWrapper here
                taskWrapper.getProperties().putInt(MarsTaskProperty.OPTIONS_TASK_ID, taskID);

            } catch (Exception e) { // RemoteExceptionHandler
                e.printStackTrace();
            }

        } catch (Exception e) {
            //
        }
    }

这个方法里面会从任务队列取出一个任务,然后最终会通过 MarsService.send 方法发送消息任务,并保存任务 id

另外这个方法开始还有启动服务的判断,由gClassName = SERVICE_DEFAULT_CLASSNAME = "com.tencent.mars.sample.wrapper.service.MarsServiceNative"知道,启动的服务是MarsServiceNative,那 MarsServiceNative 是怎样和 MarsServiceProxy 关联起来的呢?

再看 MarsServiceProxy 的 onServiceConnected 方法,MarsService 的初始化是通过MarsService.Stub.asInterface(binder)关联了 MarsServiceProxy 的 IBinder

    @Override
    public void onServiceConnected(ComponentName componentName, IBinder binder) {
        Log.d(TAG, "remote mars service connected");

        try {
            service = MarsService.Stub.asInterface(binder);
            service.registerPushMessageFilter(filter);
            service.setAccountInfo(accountInfo.uin, accountInfo.userName);

        } catch (Exception e) {
            service = null;
        }
    }

那哪个类实现了 MarsService 的方法呢?查看 MarsServiceNative 源码就能明白了,它同时实现 MarsService,间接和 MarsServiceProxy 形成强关联

public class MarsServiceNative extends Service implements MarsService {

    private static final String TAG = "Mars.Sample.MarsServiceNative";

    private MarsServiceStub stub;
    //mars服务配置工厂
    private static MarsServiceProfileFactory gFactory = new MarsServiceProfileFactory() {
        @Override
        public MarsServiceProfile createMarsServiceProfile() {
            return new DebugMarsServiceProfile();
        }
    };

      //……

    @Override
    public IBinder asBinder() {
        return stub;
    }

    /**
     * 创建运行 Mars 的服务时就初始化 STN (建议在程序启动时或者使用网络之前调用)
     */
    @Override
    public void onCreate() {
        super.onCreate();

        final MarsServiceProfile profile = gFactory.createMarsServiceProfile();
        stub = new MarsServiceStub(this, profile);

        // set callback
        AppLogic.setCallBack(stub);
        StnLogic.setCallBack(stub);
        SdtLogic.setCallBack(stub);

        // Initialize the Mars PlatformComm
        Mars.init(getApplicationContext(), new Handler(Looper.getMainLooper()));

        // Initialize the Mars
        StnLogic.setLonglinkSvrAddr(profile.longLinkHost(), profile.longLinkPorts());
        StnLogic.setShortlinkSvrAddr(profile.shortLinkPort());
        StnLogic.setClientVersion(profile.productID());
        Mars.onCreate(true);

        StnLogic.makesureLongLinkConnected();

        //
        Log.d(TAG, "mars service native created");
    }

    @Override
    public IBinder onBind(Intent intent) {
        return stub;
    }
}

MarsServiceNative 是使用 mars-core 的关键类,同时它也是一个 Service 类,运行在独立进程中,主要负责了以下功能:

  • 创建配置信息类 MarsServiceProfile,并在 StnLogic 设置相关信息;
  • 实例化一个 MarsServiceStub;
  • 设置了 AppLogic, StnLogic, SdtLogic 的回调;
  • 初始化 Mars;
  • 确定 StnLogic 的长连接StnLogic.makesureLongLinkConnected();
    既然 MarsServiceNative 设置了 AppLogic, StnLogic, SdtLogic 的回调,那再看看 MarsServiceStub 是如何实现它们接口的

4. MarsServiceStub

先看发送消息的 send 方法

public class MarsServiceStub extends MarsService.Stub implements StnLogic.ICallBack, SdtLogic.ICallBack, AppLogic.ICallBack {

    private static final String TAG = "Mars.Sample.MarsServiceStub";

    private final MarsServiceProfile profile;

    private AppLogic.AccountInfo accountInfo = new AppLogic.AccountInfo();

      //……    

    private ConcurrentLinkedQueue<MarsPushMessageFilter> filters = new ConcurrentLinkedQueue<>();

    private int clientVersion = 200;

    public MarsServiceStub(Context context, MarsServiceProfile profile) {
        this.context = context;
        this.profile = profile;
    }

    private static final int FIXED_HEADER_SKIP = 4 + 2 + 2 + 4 + 4;

    private static Map<Integer, MarsTaskWrapper> TASK_ID_TO_WRAPPER = new ConcurrentHashMap<>();

    @Override
    public int send(final MarsTaskWrapper taskWrapper, Bundle taskProperties) throws RemoteException {
        final StnLogic.Task _task = new StnLogic.Task(StnLogic.Task.EShort, 0, "", null);  //初始化为

        // Set host & cgi path
        final String host = taskProperties.getString(MarsTaskProperty.OPTIONS_HOST);
        final String cgiPath = taskProperties.getString(MarsTaskProperty.OPTIONS_CGI_PATH);
        _task.shortLinkHostList = new ArrayList<>();
        _task.shortLinkHostList.add(host);
        _task.cgi = cgiPath;

        final boolean shortSupport = taskProperties.getBoolean(MarsTaskProperty.OPTIONS_CHANNEL_SHORT_SUPPORT, true);
        final boolean longSupport = taskProperties.getBoolean(MarsTaskProperty.OPTIONS_CHANNEL_LONG_SUPPORT, false);
        if (shortSupport && longSupport) {
            _task.channelSelect = StnLogic.Task.EBoth;

        } else if (shortSupport) {
            _task.channelSelect = StnLogic.Task.EShort;

        } else if (longSupport) {
            _task.channelSelect = StnLogic.Task.ELong;

        } else {
            Log.e(TAG, "invalid channel strategy");
            throw new RemoteException("Invalid Channel Strategy");
        }

        // Set cmdID if necessary
        int cmdID = taskProperties.getInt(MarsTaskProperty.OPTIONS_CMD_ID, -1);
        if (cmdID != -1) {
            _task.cmdID = cmdID;
        }

        TASK_ID_TO_WRAPPER.put(_task.taskID, taskWrapper);

        // Send 发送任务
        Log.i(TAG, "now start task with id %d", _task.taskID);
        StnLogic.startTask(_task);
        if (StnLogic.hasTask(_task.taskID)) {
            Log.i(TAG, "stn task started with id %d", _task.taskID);

        } else {
            Log.e(TAG, "stn task start failed with id %d", _task.taskID);
        }

        return _task.taskID;
    }
    //……
}
  • 1)创建一个 StnLogic.Task,并通过 Bundle 传过来的数据(主机名、路径、长短连接和 cmdId),设置 task;
  • 2)保存 taskID 和 MarsTaskWrapper 的映射关系;
  • 3)调用 StnLogic.startTask(_task) 启动任务执行,最后返回 taskID;
    具体的逻辑实现还是在 mars-core 和底层的 C++ 源码中,这个以后研究到底层源码再说。
    在 mars-core 大概看下 StnLogic.ICallBack 接口有哪些方法:
/**
     * Created by caoshaokun on 16/2/1.
     *
     * APP使用信令通道必须实现该接口
     * 接口用于信令通道处理完后回调上层
     */
    public interface ICallBack {
        /**
         * SDK要求上层做认证操作(可能新发起一个AUTH CGI)
         * @return
         */
        boolean makesureAuthed();

        /**
         * SDK要求上层做域名解析.上层可以实现传统DNS解析,或者自己实现的域名/IP映射
         * @param host
         * @return
         */
        String[] onNewDns(final String host);

        /**
         * 收到SVR PUSH下来的消息
         * @param cmdid
         * @param data
         */
        void onPush(final int cmdid, final byte[] data);

        /**
         * SDK要求上层对TASK组包
         * @param taskID    任务标识
         * @param userContext
         * @param reqBuffer 组包的BUFFER
         * @param errCode   组包的错误码
         * @return
         */
        boolean req2Buf(final int taskID, Object userContext, ByteArrayOutputStream reqBuffer, int[] errCode, int channelSelect);

        /**
         * SDK要求上层对TASK解包
         * @param taskID        任务标识
         * @param userContext
         * @param respBuffer    要解包的BUFFER
         * @param errCode       解包的错误码
         * @return  int
         */
        int buf2Resp(final int taskID, Object userContext, final byte[] respBuffer, int[] errCode, int channelSelect);

        /**
         * 任务结束回调
         * @param taskID            任务标识
         * @param userContext
         * @param errType           错误类型
         * @param errCode           错误码
         * @return
         */
        int onTaskEnd(final int taskID, Object userContext, final int errType, final int errCode);

        /**
         * 流量统计
         * @param send
         * @param recv
         */
        void trafficData(final int send, final int recv);

        /**
         * 连接状态通知
         * @param status    综合状态,即长连+短连的状态
         * @param longlinkstatus    仅长连的状态
         */
        void reportConnectInfo(int status, int longlinkstatus);

        /**
         * SDK要求上层生成长链接数据校验包,在长链接连接上之后使用,用于验证SVR身份
         * @param identifyReqBuf    校验包数据内容
         * @param hashCodeBuffer    校验包的HASH
         * @param reqRespCmdID      数据校验的CMD ID
         * @return  ECHECK_NOW(需要校验), ECHECK_NEVER(不校验), ECHECK_NEXT(下一次再询问)
         */
        int getLongLinkIdentifyCheckBuffer(ByteArrayOutputStream identifyReqBuf, ByteArrayOutputStream hashCodeBuffer, int[] reqRespCmdID);

        /**
         * SDK要求上层解连接校验回包.
         * @param buffer            SVR回复的连接校验包
         * @param hashCodeBuffer    CLIENT请求的连接校验包的HASH值
         * @return
         */
        boolean onLongLinkIdentifyResp(final byte[] buffer, final byte[] hashCodeBuffer);

        /**
         * 请求做sync
         */
        void requestDoSync();
        String[] requestNetCheckShortLinkHosts();
        /**
         * 是否登录
         * @return true 登录 false 未登录
         */
        boolean isLogoned();

        void reportTaskProfile(String taskString);
    }

总结一下:

  • 1.NanoMarsTaskWrapper:涉及编码前、解码后和任务结束后的回调,还有字节流数组和 protobuf 对象的转换等;
  • 2.MarsServiceProxy:是一个 ServiceConnection,涉及消息的发送和取消等,作为一个 API 的功能,本质是 MarsServiceNative 代理服务类;
  • 3.MarsServiceNative 是 wrapper 的核心类,里面涉及 Mars 的初始化,设置了 AppLogic, StnLogic 和 SdtLogic 的回调等;
  • 4.而 MarsServiceStub 实现了AppLogic, StnLogic 和 SdtLogic 的回调接口,维护了和 mars-core 的调用等;

其余分析,可以到我 fork 的 mars 的地址GitHub - navyifanr/mars: (添加注释分析)下载。

参考资料:

  • GitHub - Tencent/mars: Mars is a cross-platform network component developed by WeChat.
  • 微信开源mars源码分析1—上层samples分析 - ameise_w - SegmentFault
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

微信 Mars Android Sample 源码分析 的相关文章

  • 我们如何从 Android 通知中提取 bigpicturestyle 图像?

    我有一个通知侦听器服务 可以读取来自其他应用程序的通知 经用户许可 并提取所有数据 能够访问除通知展开视图中显示的图像之外的所有内容 我也在阅读 EXTRA PICTURE 意图值 if extras containsKey Notific
  • 如何使用 kotlin Android 从 Url 读取 JSON?

    我正在使用 kotlin 来开发应用程序 现在我想从服务器获取 JSON 数据 在java中 我实现了Asyntask以及Rxjava来从Url读取JSON 我也在谷歌搜索 但我无法获得满足我的要求的正确详细信息 如何使用 kotlin 从
  • 使用holoeverywhere滑块插件时如何从活动中获取当前可见的片段?

    我想知道如何执行这些操作无处不在的全息 https github com Prototik HoloEverywhere 将滑块插件与 tabber 结合使用时 从活动中获取对当前可见和活动片段的引用 从活动 其他片段获取对 TabsTab
  • 使用 NEON 内在函数除以浮点数

    我当时正在处理四个像素的图像 这是在armv7对于 Android 应用程序 我想分一个float32x4 t向量由另一个向量组成 但其中的数字与大约不同0 7 to 3 85 在我看来 除法的唯一方法是使用右移 但这是针对一个数字2 n
  • 在Android中使用RxJava2插入SQLiteDatabase

    我在学习RxJava2在安卓中 谁能解释一下我们如何使用将数据插入 SQLiteDatabaseRxJava2 这是我尝试使用的代码示例 但它将数据插入数据库六次 单击时 getCompletableObservable subscribe
  • SQLite FTS4 使用特殊字符进行搜索

    我有一个 Android 应用程序 它使用 FTS4 虚拟表在 SQLite 数据库中搜索数据 它工作正常 但是当表中的数据包含特殊字符 如 或 时 SQLite MATCH 函数不会给出任何结果 我现在迷路了 谢谢 注意 默认的分词器真的
  • Android 辅助功能服务检测通知

    我试图让我的应用程序在显示通知时进行检测 我已在设置应用程序中启用它并且onServiceConnected确实接到电话 但是当我创建通知或通过 gmail 应用程序接收电子邮件时 什么也没有发生 onAccessibilityEvent没
  • Cordova + android:无法从应用程序打开拨号盘或邮件意图

    我有一个奇怪的问题 我无法从应用程序中打开带有预定义号码或邮件意图的拨号盘 我正在使用 netbeans 8 0 1 创建 cordova 应用程序 我的 Cordova 版本是 4 0 0 我按照步骤创建了一个应用程序 并选择了 Hell
  • 警报对话框中的 Webview 不显示内容

    我正在开发一个 Android 应用程序 我需要在网络视图和警报对话框上显示一个网站 该站点显示在网络视图中 但不显示在警报对话框中 到目前为止 这是我的代码 WebView WebView myWebView WebView v find
  • Android NDK 支持区域设置吗?

    我真正想做的就是使用格式化日期strftime x 以正确的顺序 在大多数平台上调用setlocale 足够 在 Android 上 我不断收到 美国日期 那么 Android 不支持语言环境吗 No setlocale and strft
  • Android 无法解析日期异常

    当尝试解析发送到我的 Android 客户端的日期字符串时 我得到一个无法解析的日期 这是例外 java text ParseException 无法解析的日期 2018 09 18T00 00 00Z 位于 偏移量 19 在 java t
  • 将人类日期(当地时间 GMT)转​​换为日期

    我正在服务器上工作 服务器正在向我发送 GMT 本地日期的日期 例如Fri Jun 22 09 29 29 NPT 2018在字符串格式上 我将其转换为日期 如下所示 SimpleDateFormat simpleDateFormat ne
  • Android:如何创建模态进度“轮”叠加层?

    我想在我的视图上显示模式进度 轮子 叠加层 ProgressDialog 很接近 但我不想要对话框背景或边框 我尝试设置对话框窗口的背景可绘制 this progressDialog new ProgressDialog Main this
  • Android TextureView 和硬件加速

    我正在尝试实现上所示的示例这一页 http developer android com reference android view TextureView html 我已经在运行 android 4 及以上版本的三种不同设备上进行了尝试
  • JetPack Compose - 卡中行中的weight() 不起作用

    创建 Android 应用程序时 我将一些可组合项放在卡片的一行中 如下所示 但它没有按我的预期工作 我添加 weight 1f 的可组合项不再显示 data class Test val title String val text Str
  • Activity 暂停时调用 FragmentManager.popBackStack 是否安全

    的文档FragmentManager popBackStack https developer android com reference android app FragmentManager html popBackStack java
  • 如何在android中安装和使用couch db

    我应该如何在 android 中安装和使用 couch Db 我的意思是本地沙发数据库 我可以在平板电脑和模拟器中使用它 为此我必须遵循哪些步骤 我目前正在开发一个使用它的项目 有两种选择 1 couchbase android 是的 co
  • 在线性布局内的 ScrollView 内并排对齐 TextView

    我有一个带有滚动视图的线性布局 我想保留它的当前格式 但只需将 textView2a 和 textView3a 并排放置 而不会破坏我当前的布局格式 我已经包含了我最近的尝试 但它们似乎不正确 提前致谢 Java菜鸟 当前有效的 XML
  • Android Espresso 单击按钮时出现错误

    我正在尝试使用 espresso 框架为 Android 应用程序编写一些 UI 测试 现在我只是检查启动屏幕上是否存在所有元素 然后尝试单击登录按钮 单击按钮时 测试由于错误而失败 我似乎无法理解为什么会发生这种情况 我的测试代码是 Ru
  • View.post(),以及当Runnables被执行时

    我最初的问题是需要知道我的根的高度和宽度View这样我就可以进行程序化的布局更改 就我的目的而言 我不一定需要在onCreate 对于我来说 以编程方式添加我的孩子就足够了View根布局完成后 因此我很乐意使用onWindowFocusCh

随机推荐

  • shell工具--sed和awk详解

    grep grep是一款强大的文本过滤工具 xff0c 按照关键字或者正则表达式进行过滤 具体讲解请看博文 这里写链接内容 sed sed是一种流编辑器 xff0c 它是文本处理中非常中的工具 xff0c 能够完美的配合正则表达式使用 1
  • js实现打字机效果---Day06

    我常想象这样一幅画面 xff1a 素雅的大背景 xff0c 伴着可心的音乐 xff0c 优雅旋转着的芭蕾舞者 xff0c 旁边那不断打出的文字 xff0c 仿佛就这样娓娓道来他们那美美的故事 xff1b 也常想象 xff1a 呼喇啦甩动的大
  • 纯css3实现漂亮的对话框----Day07

    姑且先不来讨论css3跟css的区别 xff0c 也不说html和html5的不同 xff0c 虽然这很关键 xff0c 但是现阶段还真的没整利落了 xff0c 姑且就这些应用先用着 xff0c 等自己有些见解了再来探讨那些深层次的问题 先
  • 纯css3实现饼状图-------Day21

    现代网站在商务应用中比较广泛 xff0c 什么oa xff0c 什么erp xff0c 除了导入导出 xff0c 就是数据统计 xff0c 再不然就来个做个报表 xff0c 而饼状图作为数据的一种直观统计显示 xff0c 应用是非常广泛的
  • 你是如何理解var e=e||window.event的------Day26

    你是如何理解var e 61 e window event的 xff1f 相信很多人都能给我个回答说是 xff1a 为了实现多种浏览器兼容 不错 xff0c 确实是为了实现浏览器兼容 xff0c 但是它又是如何实现浏览器兼容的呢 xff1f
  • js实现回放拖拽轨迹-------Day48

    今天有点小高兴 xff0c csdn博客浏览量过万了 xff0c 在过去还从来没有过这么高的浏览量呢 xff0c 不得不说 xff0c 太多时候还是有些矫情 xff0c 可看到这些鼓励还是忍不住高兴啊 xff0c 至少 xff0c 这样让我
  • js实现动态删除表格的行或者列-------Day57

    昨天记录了动态添加表格的一行 xff0c 当然这个一行是指一行数据 xff0c 也就是说一行多少列也是加上的 xff0c 并且第几列的内容都可以添加上 xff0c 先来回顾下它的实现的关键点 xff1a 1 var row 61 table
  • js实现表格的选中一行-------Day58

    最开始想更多的用js来动态操作表格 xff0c 是因为在应用了easyUI之后 xff0c 发现直接写一个 lt table id 61 34 tt 34 gt lt table gt xff0c 这就够了 xff0c 界面里面就剩下这么一
  • 积跬步,聚小流-------关于UML时序图

    uml时序图的存在 在上一篇中记录了uml类图 xff0c 用类图来描述代码中所需要的类以及相互之间的关系 xff0c 也就立体的将整个程序框架展现在了我们面前 xff0c 像一幅画 xff0c 有山有水有人 一张照片只能定格当时的美好 x
  • 积跬步,聚小流------用smartpaginator来做分页

    网络上的实例 xff08 jquery smarypaginator 例图 xff09 如果说是从 百度 上搜索过 jquery分页插件 的朋友 xff0c 相信对上面的图片不会陌生 xff0c 几乎所有介绍 jquery分页插件 的文章中
  • 我的2017-搭建个人网站,搭建PHP环境(2)

    上周确定了 xff0c 想要应用的后台语言 xff0c 面临的最大问题就是 xff1a php我不会啊 xff0c 哈哈哈哈 xff0c 所以接下来首先要做的就是了解 学习php的相关知识 接下来的第一步 xff1a 环境搭建 1 下载安装
  • 设计一个类:只能在堆上创建对象?只能在栈上创建对象?只能创建一个对象?

    在C 43 43 中 xff0c 类的对象建立分为两种 xff0c 一种是静态建立 xff0c 如A a xff1b 另一种是动态建立 xff0c 如A ptr 61 new A xff1b 这两种方式是有区别的 静态建立一个类对象 xff
  • 我的2017-搭建个人网站,hello PHP(2)

    学习一门语言 xff0c 例行惯例 xff0c 先来个 hello world 搭建好了php环境 xff0c 然后就可以运行php了 xff0c 首先用一种最简单的方法 xff0c 在wamp安装位置 xff08 相应的文件夹 xff09
  • 我的2017-搭建个人网站,自拟定代码根目录

    wampserver集成安装环境安装的php的运行根目录在wamp文件夹中的www文件夹下 xff0c 而为了有效的将代码和服务器进行分离 xff0c 可以采用自拟定代码根目录进行修改 1 确定代码编辑位置 xff0c 修改服务器默认指向
  • 编译原理:求First集与Follow集的方法

    明天就要考试了 xff0c 发现一直理解错了First集与Follow集的解法 xff0c 贴上比较好理解的 文法 xff1a S ABc A a B b First集合求法 能 由非终结符号推出的所有的开头符号或可能的 xff0c 但要求
  • 位运算n & (n-1)的妙用

    本文转自 xff1a http blog csdn net zheng0518 article details 8882394 按位与的知识 n amp n 1 作用 xff1a 将n的二进制表示中的最低位为1的改为0 xff0c 先看一个
  • 二分查找算法(Java版)

    二分查找算法是非常经典且基本的算法 1 二分查找又称折半查找 xff0c 优点是比较次数少 xff0c 查找速度快 xff0c 平均性能好 xff1b 其缺点是要求待查表为有序表 xff0c 且插入删除困难 因此 xff0c 折半查找方法适
  • 电脑眼睛保护色——绿豆沙色

    眼科医生建议电脑屏幕不要用白色 xff0c 因为白色对眼睛的刺激是最大的 xff0c 最损伤视力 xff01 眼科医生推荐的颜色是 柔和的淡绿色 xff0c 也有人叫做绿豆沙色 按HSU 设置为 xff1a 色度 HUE xff1a 85
  • Java中两个数交换的细节问题

    在码代码过程中 xff0c 两个数交换是经常用到的 xff0c 但在Java 中 xff0c 如果忽略了值传递和引用传递就很容易出错 看一下两个整数交换代码 xff1a public void swap int a int b int t
  • 微信 Mars Android Sample 源码分析

    注 xff1a 原文首发地址 零 前言 Mars 是微信官方开源的跨平台跨业务的终端基础组件 xff0c 具有高质量网络连接模块 长短连接 智能心跳机制 高性能日志模块和网络监测组件等 而整个 Android Sample 是基于 Mars